/* 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 DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
loader.lazyRequireGetter(
  this,
  "ObjectUtils",
  "resource://devtools/server/actors/object/utils.js"
);
const {
  formatDisplayName,
} = require("resource://devtools/server/actors/frame.js");
const {
  TYPES,
  getResourceWatcher,
} = require("resource://devtools/server/actors/resources/index.js");
loader.lazyRequireGetter(
  this,
  ["isValidSavedFrame"],
  "devtools/server/actors/frame",
  true
);

// Get a string message to display when a frame evaluation throws.
function getThrownMessage(completion) {
  try {
    if (completion.throw.getOwnPropertyDescriptor) {
      return completion.throw.getOwnPropertyDescriptor("message").value;
    } else if (completion.toString) {
      return completion.toString();
    }
  } catch (ex) {
    // ignore
  }
  return "Unknown exception";
}
module.exports.getThrownMessage = getThrownMessage;

function evalAndLogEvent({
  threadActor,
  frame,
  level,
  expression,
  bindings,
  showStacktrace,
}) {
  const frameLocation = threadActor.sourcesManager.getFrameLocation(frame);
  const { sourceActor, line } = frameLocation;
  const displayName = formatDisplayName(frame);
  const stacktrace = showStacktrace ? [] : undefined;

  if (showStacktrace) {
    let currentFrame = frame;
    while (currentFrame) {
      if (currentFrame.script) {
        stacktrace.push({
          filename: currentFrame.script.url,
          functionName: currentFrame.script.displayName,
          lineNumber: currentFrame.script.startLine,
          columnNumber: currentFrame.script.startColumn,
          sourceId: currentFrame.script.source.id,
        });
      } else {
        stacktrace.push({
          asyncCause: currentFrame.asyncCause,
          filename: currentFrame.source || "unknown",
          functionName:
            currentFrame.functionDisplayName ||
            currentFrame.displayName ||
            "anonymous",
          lineNumber: currentFrame.line,
          columnNumber: currentFrame.column,
          sourceId: currentFrame.sourceId,
        });
      }
      const olderSavedFrame =
        currentFrame.olderSavedFrame &&
        isValidSavedFrame(threadActor, currentFrame.olderSavedFrame)
          ? currentFrame.olderSavedFrame
          : null;
      currentFrame =
        currentFrame.older ||
        currentFrame.parent ||
        currentFrame.asyncParent ||
        olderSavedFrame;
    }
  }

  // TODO remove this branch when (#1749668) lands (#1609540)
  if (isWorker) {
    threadActor.targetActor._consoleActor.evaluateJS({
      text: showStacktrace
        ? `console.trace(...${expression})`
        : `console.log(...${expression})`,
      bindings: { displayName, ...bindings },
      url: sourceActor.url,
      lineNumber: line,
      disableBreaks: true,
    });

    return undefined;
  }

  let completion;
  // Ensure disabling all types of breakpoints for all sources while evaluating the log points
  threadActor.insideClientEvaluation = { disableBreaks: true };
  try {
    completion = frame.evalWithBindings(
      expression,
      {
        displayName,
        ...bindings,
      },
      { hideFromDebugger: true }
    );
  } finally {
    threadActor.insideClientEvaluation = null;
  }

  let value;
  if (!completion) {
    // The evaluation was killed (possibly by the slow script dialog).
    value = ["Evaluation failed"];
  } else if ("return" in completion) {
    value = [];
    const length = ObjectUtils.getArrayLength(completion.return);
    for (let i = 0; i < length; i++) {
      value.push(DevToolsUtils.getProperty(completion.return, i));
    }
  } else {
    value = [getThrownMessage(completion)];
    level = `${level}Error`;
  }

  ChromeUtils.addProfilerMarker("Debugger log point", undefined, value);

  emitConsoleMessage(threadActor, frameLocation, value, level, stacktrace);

  return undefined;
}

function logEvent({ threadActor, frame }) {
  const frameLocation = threadActor.sourcesManager.getFrameLocation(frame);
  const { sourceActor, line } = frameLocation;

  // TODO remove this branch when (#1749668) lands (#1609540)
  if (isWorker) {
    const bindings = {};
    for (let i = 0; i < frame.arguments.length; i++) {
      bindings[`x${i}`] = frame.arguments[i];
    }
    threadActor.targetActor._consoleActor.evaluateJS({
      text: `console.log(${Object.keys(bindings).join(",")})`,
      bindings,
      url: sourceActor.url,
      lineNumber: line,
      disableBreaks: true,
    });

    return undefined;
  }

  emitConsoleMessage(threadActor, frameLocation, frame.arguments, "logPoint");

  return undefined;
}

function emitConsoleMessage(
  threadActor,
  frameLocation,
  args,
  level,
  stacktrace
) {
  const targetActor = threadActor.targetActor;
  const { sourceActor, line, column } = frameLocation;

  const message = {
    filename: sourceActor.url,

    // The line is 1-based
    lineNumber: line,
    // `frameLocation` comes from the SourcesManager which uses 0-base column
    // whereas CONSOLE_MESSAGE resources emits 1-based columns.
    columnNumber: column + 1,
    arguments: args,
    level,
    timeStamp: ChromeUtils.dateNow(),
    chromeContext:
      targetActor.actorID &&
      /conn\d+\.parentProcessTarget\d+/.test(targetActor.actorID),
    // The 'prepareConsoleMessageForRemote' method in webconsoleActor expects internal source ID,
    // thus we can't set sourceId directly to sourceActorID.
    sourceId: sourceActor.internalSourceId,
    stacktrace,
  };

  // Note that only WindowGlobalTarget actor support resource watcher
  // This is still missing for worker and content processes
  const consoleMessageWatcher = getResourceWatcher(
    targetActor,
    TYPES.CONSOLE_MESSAGE
  );
  if (consoleMessageWatcher) {
    consoleMessageWatcher.emitMessages([message], false);
  } else {
    // Bug 1642296: Once we enable ConsoleMessage resource on the server, we should remove onConsoleAPICall
    // from the WebConsoleActor, and only support the ConsoleMessageWatcher codepath.
    message.arguments = message.arguments.map(arg =>
      arg && typeof arg.unsafeDereference === "function"
        ? arg.unsafeDereference()
        : arg
    );
    targetActor._consoleActor.onConsoleAPICall(message);
  }
}

module.exports.evalAndLogEvent = evalAndLogEvent;
module.exports.logEvent = logEvent;
