// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import type * as Protocol from '../../../generated/protocol.js';
import type * as Handlers from '../handlers/handlers.js';
import * as Helpers from '../helpers/helpers.js';
import * as Types from '../types/types.js';

export const stackTraceForEventInTrace =
    new Map<Handlers.Types.HandlerData, Map<Types.Events.Event, Protocol.Runtime.StackTrace>>();

export function clearCacheForTrace(data: Handlers.Types.HandlerData): void {
  stackTraceForEventInTrace.delete(data);
}
/**
 * This util builds a stack trace that includes async calls for a given
 * event. It leverages data we collect from sampling to deduce sync
 * stacks and trace event instrumentation on the V8 debugger to stitch
 * them together.
 */
export function get(event: Types.Events.Event, data: Handlers.Types.HandlerData): Protocol.Runtime.StackTrace|null {
  let cacheForTrace = stackTraceForEventInTrace.get(data);
  if (!cacheForTrace) {
    cacheForTrace = new Map();
    stackTraceForEventInTrace.set(data, cacheForTrace);
  }
  const resultFromCache = cacheForTrace.get(event);
  if (resultFromCache) {
    return resultFromCache;
  }
  let result: Protocol.Runtime.StackTrace|null = null;
  if (Types.Extensions.isSyntheticExtensionEntry(event)) {
    result = getForExtensionEntry(event, data);
  } else if (Types.Events.isPerformanceMeasureBegin(event)) {
    result = getForPerformanceMeasure(event, data);
  } else {
    result = getForEvent(event, data);
    const payloadCallFrames =
        getTraceEventPayloadStackAsProtocolCallFrame(event).filter(callFrame => !isNativeJSFunction(callFrame));
    // If the event has a payload stack trace, replace the synchronous
    // portion of the calculated stack with the payload's call frames.
    // We do this because trace payload call frames contain call
    // locations, unlike profile call frames obtained with getForEvent
    // (which contain function declaration locations).
    // This way the user knows which exact JS location triggered an
    // event.
    if (!result.callFrames.length) {
      result.callFrames = payloadCallFrames;
    } else {
      for (let i = 0; i < payloadCallFrames.length && i < result.callFrames.length; i++) {
        result.callFrames[i] = payloadCallFrames[i];
      }
    }
  }
  if (result) {
    cacheForTrace.set(event, result);
  }
  return result;
}

/**
 * Fallback method to obtain a stack trace using the parsed event tree
 * hierarchy. This shouldn't be called outside of this file, use `get`
 * instead to ensure the correct event in the tree hierarchy is used.
 */
function getForEvent(event: Types.Events.Event, data: Handlers.Types.HandlerData): Protocol.Runtime.StackTrace {
  // When working with a CPU profile the renderer handler won't have
  // entries in its tree.
  const entryToNode = data.Renderer.entryToNode.size > 0 ? data.Renderer.entryToNode : data.Samples.entryToNode;
  const topStackTrace: Protocol.Runtime.StackTrace = {callFrames: []};
  let stackTrace: Protocol.Runtime.StackTrace = topStackTrace;
  let currentEntry: Types.Events.SyntheticProfileCall;
  let node: Helpers.TreeHelpers.TraceEntryNode|null|undefined = entryToNode.get(event);
  const traceCache = stackTraceForEventInTrace.get(data) || new Map<Types.Events.Event, Protocol.Runtime.StackTrace>();
  stackTraceForEventInTrace.set(data, traceCache);
  // Move up this node's ancestor tree appending JS frames to its
  // stack trace. If an async caller is detected, move up in the async
  // stack instead.
  while (node) {
    if (!Types.Events.isProfileCall(node.entry)) {
      const maybeAsyncParent = data.AsyncJSCalls.runEntryPointToScheduler.get(node.entry);
      if (!maybeAsyncParent) {
        node = node.parent;
        continue;
      }
      const maybeAsyncParentNode = maybeAsyncParent && entryToNode.get(maybeAsyncParent.scheduler);
      if (maybeAsyncParentNode) {
        stackTrace = addAsyncParentToStack(stackTrace, maybeAsyncParent.taskName);
        node = maybeAsyncParentNode;
      }
      continue;
    }
    currentEntry = node.entry;
    // First check if this entry was processed before.
    const stackTraceFromCache = traceCache.get(node.entry);
    if (stackTraceFromCache) {
      stackTrace.callFrames.push(...stackTraceFromCache.callFrames.filter(callFrame => !isNativeJSFunction(callFrame)));
      stackTrace.parent = stackTraceFromCache.parent;
      // Only set the description to the cache value if we didn't
      // compute it in the previous iteration, since the async stack
      // trace descriptions / taskNames is only extracted when jumping
      // to the async parent, and that might not have happened when
      // the cached value was computed (e.g. the cached value
      // computation started at some point inside the parent stack
      // trace).
      stackTrace.description = stackTrace.description || stackTraceFromCache.description;
      break;
    }

    if (!isNativeJSFunction(currentEntry.callFrame)) {
      stackTrace.callFrames.push(currentEntry.callFrame);
    }
    const maybeAsyncParentEvent = data.AsyncJSCalls.asyncCallToScheduler.get(currentEntry);
    const maybeAsyncParentNode = maybeAsyncParentEvent && entryToNode.get(maybeAsyncParentEvent.scheduler);
    if (maybeAsyncParentNode) {
      stackTrace = addAsyncParentToStack(stackTrace, maybeAsyncParentEvent.taskName);
      node = maybeAsyncParentNode;
      continue;
    }
    node = node.parent;
  }
  return topStackTrace;
}

function addAsyncParentToStack(stackTrace: Protocol.Runtime.StackTrace, taskName: string): Protocol.Runtime.StackTrace {
  const parent: Protocol.Runtime.StackTrace = {callFrames: []};
  // The Protocol.Runtime.StackTrace type is recursive, so we
  // move one level deeper in it as we walk up the ancestor tree.
  stackTrace.parent = parent;
  // Note: this description effectively corresponds to the name
  // of the task that scheduled the stack trace we are jumping
  // FROM, so it would make sense that it was set to that stack
  // trace instead of the one we are jumping TO. However, the
  // JS presentation utils we use to present async stack traces
  // assume the description is added to the stack trace that
  // scheduled the async task, so we build the data that way.
  parent.description = taskName;
  return parent;
}

/**
 * Finds the JS call in which an extension entry was injected (the
 * code location that called the extension API), and returns its stack
 * trace.
 */
function getForExtensionEntry(event: Types.Extensions.SyntheticExtensionEntry, data: Handlers.Types.HandlerData):
    Protocol.Runtime.StackTrace|null {
  const rawEvent: Types.Events.Event = event.rawSourceEvent;
  if (Types.Events.isPerformanceMeasureBegin(rawEvent)) {
    return getForPerformanceMeasure(rawEvent, data);
  }
  if (!rawEvent) {
    return null;
  }
  return get(rawEvent, data);
}

/**
 * Gets the raw event for a user timing and obtains its stack trace.
 */
function getForPerformanceMeasure(
    event: Types.Events.PerformanceMeasureBegin, data: Handlers.Types.HandlerData): Protocol.Runtime.StackTrace|null {
  let rawEvent: Types.Events.Event|undefined = event;
  if (event.args.traceId === undefined) {
    return null;
  }
  // performance.measure calls dispatch 2 events: one for the call
  // itself and another to represent the measured entry in the trace
  // timeline. They are connected via a common traceId. At this
  // point `rawEvent` corresponds to the second case, we must
  // encounter the event for the call itself to obtain its callstack.
  rawEvent = data.UserTimings.measureTraceByTraceId.get(event.args.traceId);
  if (!rawEvent) {
    return null;
  }
  return get(rawEvent, data);
}
/**
 * Determines if a function is a native JS API (like setTimeout,
 * requestAnimationFrame, consoleTask.run. etc.). This is useful to
 * discard stack frames corresponding to the JS scheduler function
 * itself, since it's already being used as title of async stack traces
 * taken from the async `taskName`. This is also consistent with the
 * behaviour of the stack trace in the sources
 * panel.
 */
function isNativeJSFunction({columnNumber, lineNumber, url, scriptId}: Protocol.Runtime.CallFrame): boolean {
  return lineNumber === -1 && columnNumber === -1 && url === '' && scriptId === '0';
}

/**
 * Converts a stack trace from a trace event's payload into an array of
 * Protocol.Runtime.CallFrame.
 */
function getTraceEventPayloadStackAsProtocolCallFrame(event: Types.Events.Event): Protocol.Runtime.CallFrame[] {
  const payloadCallStack = Helpers.Trace.getZeroIndexedStackTraceInEventPayload(event) || [];
  const callFrames: Protocol.Runtime.CallFrame[] = [];
  for (const frame of payloadCallStack) {
    callFrames.push({...frame, scriptId: String(frame.scriptId) as Protocol.Runtime.ScriptId});
  }
  return callFrames;
}
