/* 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 lazy = {};
ChromeUtils.defineESModuleGetters(
  lazy,
  {
    JSTracer: "resource://devtools/server/tracer/tracer.sys.mjs",
  },
  { global: "contextual" }
);

const { Actor } = require("resource://devtools/shared/protocol.js");
const { createValueGrip } = require("devtools/server/actors/object/utils");
const {
  ObjectActorPool,
} = require("resource://devtools/server/actors/object/ObjectActorPool.js");
const {
  tracerSpec,
  TRACER_LOG_METHODS,
} = require("resource://devtools/shared/specs/tracer.js");

loader.lazyRequireGetter(
  this,
  "StdoutTracingListener",
  "resource://devtools/server/actors/tracer/stdout.js",
  true
);
loader.lazyRequireGetter(
  this,
  "ResourcesTracingListener",
  "resource://devtools/server/actors/tracer/resources.js",
  true
);
loader.lazyRequireGetter(
  this,
  "ProfilerTracingListener",
  "resource://devtools/server/actors/tracer/profiler.js",
  true
);

// Indexes of each data type within the array describing a trace
exports.TRACER_FIELDS_INDEXES = {
  // This is shared with all the data types
  TYPE: 0,

  // Frame traces are slightly special and do not share any field with the other data types
  FRAME_IMPLEMENTATION: 1,
  FRAME_NAME: 2,
  FRAME_SOURCEID: 3,
  FRAME_LINE: 4,
  FRAME_COLUMN: 5,
  FRAME_URL: 6,

  // These fields are shared with all but frame data types
  PREFIX: 1,
  FRAME_INDEX: 2,
  TIMESTAMP: 3,
  DEPTH: 4,

  EVENT_NAME: 5,

  ENTER_ARGS: 5,
  ENTER_ARG_NAMES: 6,

  EXIT_PARENT_FRAME_ID: 5,
  EXIT_RETURNED_VALUE: 6,
  EXIT_WHY: 7,

  DOM_MUTATION_TYPE: 5,
  DOM_MUTATION_ELEMENT: 6,
};

const VALID_LOG_METHODS = Object.values(TRACER_LOG_METHODS);

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

  // When the tracer is stopped, save the result of the Listener Class.
  // This is used by the profiler log method and the getProfile method.
  #stopResult = null;

  // A Pool for all JS values emitted by the Tracer Actor.
  // This helps instantiate a unique Object Actor per JS Object communicated to the client.
  // This also helps share the same Object Actor instances when evaluating JS via
  // the console actor.
  // This pool is created lazily, only once we start a new trace.
  // We also clear the pool before starting the trace.
  #tracerPool = null;

  destroy() {
    this.stopTracing();
  }

  getLogMethod() {
    return this.logMethod;
  }

  /**
   * Toggle tracing JavaScript.
   * Meant for the WebConsole command in order to pass advanced
   * configuration directly to JavaScriptTracer class.
   *
   * @param {object} options
   *        Options used to configure JavaScriptTracer.
   *        See `JavaScriptTracer.startTracing`.
   * @return {boolean}
   *         True if the tracer starts, or false if it was stopped.
   */
  toggleTracing(options) {
    if (!this.tracingListener) {
      this.startTracing(options);
      return true;
    }
    this.stopTracing();
    return false;
  }

  /**
   * Start tracing.
   *
   * @param {object} options
   *        Options used to configure JavaScriptTracer.
   *        See `JavaScriptTracer.startTracing`.
   */
  // eslint-disable-next-line complexity
  startTracing(options = {}) {
    if (options.logMethod && !VALID_LOG_METHODS.includes(options.logMethod)) {
      throw new Error(
        `Invalid log method '${options.logMethod}'. Only supports: ${VALID_LOG_METHODS}`
      );
    }
    if (options.prefix && typeof options.prefix != "string") {
      throw new Error("Invalid prefix, only support string type");
    }
    if (options.maxDepth && typeof options.maxDepth != "number") {
      throw new Error("Invalid max-depth, only support numbers");
    }
    if (options.maxRecords && typeof options.maxRecords != "number") {
      throw new Error("Invalid max-records, only support numbers");
    }

    // When tracing on next user interaction is enabled,
    // disable logging from workers as this makes the tracer work
    // against visible documents and is actived per document thread.
    if (options.traceOnNextInteraction && isWorker) {
      return;
    }

    // Ignore WindowGlobal target actors for WindowGlobal of iframes running in the same process and thread as their parent document.
    // isProcessRoot will be true for each WindowGlobal being the top parent within a given process.
    // It will typically be true for WindowGlobal of iframe running in a distinct origin and process,
    // but only for the top iframe document. It will also be true for the top level tab document.
    if (
      this.targetActor.window &&
      !this.targetActor.window.windowGlobalChild?.isProcessRoot
    ) {
      return;
    }

    // Flush any previous recorded data only when we start a new tracer
    // as we may still analyse trace data after stopping the trace.
    // The pool will then be re-created on demand from createValueGrip.
    if (this.#tracerPool) {
      this.#tracerPool.destroy();
      this.#tracerPool = null;
    }

    this.logMethod = options.logMethod || TRACER_LOG_METHODS.STDOUT;

    let ListenerClass = null;
    // Currently only the profiler output is supported with the native tracer.
    let useNativeTracing = false;
    switch (this.logMethod) {
      case TRACER_LOG_METHODS.STDOUT:
        ListenerClass = StdoutTracingListener;
        break;
      case TRACER_LOG_METHODS.CONSOLE:
      case TRACER_LOG_METHODS.DEBUGGER_SIDEBAR:
        // Console and debugger sidebar are both using JSTRACE_STATE/JSTRACE_TRACE resources
        // to receive tracing data.
        ListenerClass = ResourcesTracingListener;
        break;
      case TRACER_LOG_METHODS.PROFILER:
        ListenerClass = ProfilerTracingListener;
        // Recording function returns is mandatory when recording profiler output.
        // Otherwise frames are not closed and mixed up in the profiler frontend.
        options.traceFunctionReturn = true;
        useNativeTracing = true;
        break;
    }
    this.tracingListener = new ListenerClass({
      targetActor: this.targetActor,
      traceValues: !!options.traceValues,
      traceActor: this,
    });
    lazy.JSTracer.addTracingListener(this.tracingListener);

    this.traceValues = !!options.traceValues;
    try {
      lazy.JSTracer.startTracing({
        global: this.targetActor.targetGlobal,
        prefix: options.prefix || "",
        // Enable receiving the `currentDOMEvent` being passed to `onTracingFrame`
        traceDOMEvents: true,
        // Enable tracing DOM Mutations
        traceDOMMutations: options.traceDOMMutations,
        // Enable tracing function arguments as well as returned values
        traceValues: !!options.traceValues,
        // Enable tracing only on next user interaction
        traceOnNextInteraction: !!options.traceOnNextInteraction,
        // Notify about frame exit / function call returning
        traceFunctionReturn: !!options.traceFunctionReturn,
        // Use the native tracing implementation
        useNativeTracing,
        // Ignore frames beyond the given depth
        maxDepth: options.maxDepth,
        // Stop the tracing after a number of top level frames
        maxRecords: options.maxRecords,
      });
    } catch (e) {
      // If startTracing throws, it probably rejected one of its options and we should
      // unregister the tracing listener.
      this.stopTracing();
      throw e;
    }
  }

  async stopTracing() {
    if (!this.tracingListener) {
      return;
    }
    // Remove before stopping to prevent receiving the stop notification
    lazy.JSTracer.removeTracingListener(this.tracingListener);
    // Save the result of the stop request for the profiler and the getProfile RDP method
    this.#stopResult = this.tracingListener.stop();
    this.tracingListener = null;

    lazy.JSTracer.stopTracing();
    this.logMethod = null;
  }

  /**
   * Queried by THREAD_STATE watcher to send the gecko profiler data
   * as part of THREAD STATE "stop" resource.
   *
   * @return {object} Gecko profiler profile object.
   */
  async getProfile() {
    // #stopResult is a promise
    return this.#stopResult;
  }

  createValueGrip(value) {
    if (!this.#tracerPool) {
      this.#tracerPool = new ObjectActorPool(
        this.targetActor.threadActor,
        "tracer",
        true
      );
      this.manage(this.#tracerPool);
    }
    return createValueGrip(this, value, this.#tracerPool);
  }
}
exports.TracerActor = TracerActor;
