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

import * as Platform from '../../../core/platform/platform.js';
import type * as Protocol from '../../../generated/protocol.js';
import * as CPUProfile from '../../cpu_profile/cpu_profile.js';
import * as Helpers from '../helpers/helpers.js';
import * as Types from '../types/types.js';

let profilesInProcess = new Map<Types.Events.ProcessID, Map<Types.Events.ThreadID, ProfileData>>();
let entryToNode = new Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>();

// The profile head, containing its metadata like its start
// time, comes in a "Profile" event. The sample data comes in
// "ProfileChunk" events. We match these ProfileChunks with their head
// using process and profile ids. However, in order to integrate sample
// data with trace data, we need the thread id that owns each profile.
// This thread id is extracted from the head event.
// For this reason, we have a preprocessed data structure, where events
// are matched by profile id, which we then finish processing to export
// events matched by thread id.
let preprocessedData = new Map<Types.Events.ProcessID, Map<Types.Events.ProfileID, PreprocessedData>>();

/**
 * Profile source selection priority when multiple profiles exist for the same thread.
 *
 * Profile sources and their typical scenarios:
 * - 'Internal': Browser-initiated profiling performance panel traces.
 *   This is the profiling mechanism when users click "Record" in the Devtools UI.
 * - 'Inspector': User-initiated via console.profile()/profileEnd() calls.
 *   Represents explicit developer intent to profile specific code.
 * - 'SelfProfiling': Page-initiated via JS Self-Profiling API.
 *    Lower signal vs the two above; treated as fallback.
 *
 * Selection strategy:
 * - CPU Profile mode: Prefer 'Inspector' (explicit user request).
 * - Performance trace: Prefer 'Internal' (integrated timeline context), then 'Inspector'.
 * - Sources not in the priority list (including 'SelfProfiling') act as fallbacks.
 *   When no priority source matches, the first candidate profile is selected.
 */
const PROFILE_SOURCES_BY_PRIORITY = {
  cpuProfile: ['Inspector'] as Types.Events.ProfileSource[],
  performanceTrace: ['Internal', 'Inspector'] as Types.Events.ProfileSource[],
};

function parseCPUProfileData(parseOptions: Types.Configuration.ParseOptions): void {
  const priorityList =
      parseOptions.isCPUProfile ? PROFILE_SOURCES_BY_PRIORITY.cpuProfile : PROFILE_SOURCES_BY_PRIORITY.performanceTrace;

  for (const [processId, profiles] of preprocessedData) {
    const profilesByThread =
        new Map<Types.Events.ThreadID, Array<{id: Types.Events.ProfileID, data: PreprocessedData}>>();
    for (const [profileId, preProcessedData] of profiles) {
      const threadId = preProcessedData.threadId;
      if (threadId === undefined) {
        continue;
      }
      const listForThread = Platform.MapUtilities.getWithDefault(profilesByThread, threadId, () => []);
      listForThread.push({id: profileId, data: preProcessedData});
    }

    for (const [threadId, candidates] of profilesByThread) {
      if (!candidates.length) {
        continue;
      }
      let chosen = candidates[0];
      for (const source of priorityList) {
        const match = candidates.find(p => p.data.source === source);
        if (match) {
          chosen = match;
          break;
        }
      }
      const chosenData = chosen.data;
      if (!chosenData.rawProfile.nodes.length) {
        continue;
      }
      const indexStack: number[] = [];

      const profileModel = new CPUProfile.CPUProfileDataModel.CPUProfileDataModel(chosenData.rawProfile);
      const profileTree = Helpers.TreeHelpers.makeEmptyTraceEntryTree();
      profileTree.maxDepth = profileModel.maxDepth;

      const selectedProfileId = chosen.id;
      const finalizedData: ProfileData = {
        rawProfile: chosenData.rawProfile,
        parsedProfile: profileModel,
        profileCalls: [],
        profileTree,
        profileId: selectedProfileId,
      };
      const dataByThread = Platform.MapUtilities.getWithDefault(profilesInProcess, processId, () => new Map());
      dataByThread.set(threadId, finalizedData);

      // Only need to build pure JS ProfileCalls if we're parsing a CPU Profile, otherwise SamplesIntegrator does the work.
      if (parseOptions.isCPUProfile) {
        buildProfileCallsForCPUProfile();
      }

      function buildProfileCallsForCPUProfile(): void {
        profileModel.forEachFrame(openFrameCallback, closeFrameCallback);

        function openFrameCallback(
            depth: number, node: CPUProfile.ProfileTreeModel.ProfileNode, sampleIndex: number,
            timeStampMilliseconds: number): void {
          if (threadId === undefined) {
            return;
          }
          const ts = Helpers.Timing.milliToMicro(Types.Timing.Milli(timeStampMilliseconds));
          const nodeId = node.id as Helpers.TreeHelpers.TraceEntryNodeId;

          const profileCall =
              Helpers.Trace.makeProfileCall(node, selectedProfileId, sampleIndex, ts, processId, threadId);
          finalizedData.profileCalls.push(profileCall);
          indexStack.push(finalizedData.profileCalls.length - 1);
          const traceEntryNode = Helpers.TreeHelpers.makeEmptyTraceEntryNode(profileCall, nodeId);
          entryToNode.set(profileCall, traceEntryNode);
          traceEntryNode.depth = depth;
          if (indexStack.length === 1) {
            // First call in the stack is a root call.
            finalizedData.profileTree?.roots.add(traceEntryNode);
          }
        }
        function closeFrameCallback(
            _depth: number, _node: CPUProfile.ProfileTreeModel.ProfileNode, _sampleIndex: number,
            _timeStampMillis: number, durMs: number, selfTimeMs: number): void {
          const profileCallIndex = indexStack.pop();
          const profileCall = profileCallIndex !== undefined && finalizedData.profileCalls[profileCallIndex];
          if (!profileCall) {
            return;
          }
          const {callFrame, ts, pid, tid} = profileCall;
          const traceEntryNode = entryToNode.get(profileCall);
          if (callFrame === undefined || ts === undefined || pid === undefined || selectedProfileId === undefined ||
              tid === undefined || traceEntryNode === undefined) {
            return;
          }
          const dur = Helpers.Timing.milliToMicro(Types.Timing.Milli(durMs));
          const selfTime = Helpers.Timing.milliToMicro(Types.Timing.Milli(selfTimeMs));
          profileCall.dur = dur;
          traceEntryNode.selfTime = selfTime;

          const parentIndex = indexStack.at(-1);
          const parent = parentIndex !== undefined && finalizedData.profileCalls.at(parentIndex);
          const parentNode = parent && entryToNode.get(parent);
          if (!parentNode) {
            return;
          }
          traceEntryNode.parent = parentNode;
          parentNode.children.push(traceEntryNode);
        }
      }
    }
  }
}

export function reset(): void {
  preprocessedData = new Map();
  profilesInProcess = new Map();
  entryToNode = new Map();
}

export function handleEvent(event: Types.Events.Event): void {
  /**
   * A fake trace event created to support CDP.Profiler.Profiles in the
   * trace engine.
   */
  if (Types.Events.isSyntheticCpuProfile(event)) {
    // At the moment we are attaching to a single node target so we
    // should only get a single CPU profile. The values of the process
    // id and thread id are not really important, so we use the data
    // in the fake event. Should multi-thread CPU profiling be supported
    // we could use these fields in the event to pass thread info.
    const profileData = getOrCreatePreProcessedData(event.pid, event.id);
    profileData.rawProfile = event.args.data.cpuProfile;
    profileData.threadId = event.tid;
    return;
  }

  if (Types.Events.isProfile(event)) {
    // Do not use event.args.data.startTime as it is in CLOCK_MONOTONIC domain,
    // but use profileEvent.ts which has been translated to Perfetto's clock
    // domain. Also convert from ms to us.
    // Note: events are collected on a different thread than what's sampled.
    // The correct process and thread ids are specified by the profile.
    const profileData = getOrCreatePreProcessedData(event.pid, event.id);
    profileData.rawProfile.startTime = event.ts;
    profileData.threadId = event.tid;
    assignProfileSourceIfKnown(profileData, event.args?.data?.source);
    return;
  }
  if (Types.Events.isProfileChunk(event)) {
    const profileData = getOrCreatePreProcessedData(event.pid, event.id);
    const cdpProfile = profileData.rawProfile;
    const nodesAndSamples: Types.Events.PartialProfile = event.args?.data?.cpuProfile || {samples: []};
    const samples = nodesAndSamples?.samples || [];
    const traceIds = event.args?.data?.cpuProfile?.trace_ids;
    for (const n of nodesAndSamples?.nodes || []) {
      const lineNumber = typeof n.callFrame.lineNumber === 'undefined' ? -1 : n.callFrame.lineNumber;
      const columnNumber = typeof n.callFrame.columnNumber === 'undefined' ? -1 : n.callFrame.columnNumber;

      const scriptId = String(n.callFrame.scriptId) as Protocol.Runtime.ScriptId;
      const url = n.callFrame.url || '';
      const node = {
        ...n,
        callFrame: {
          ...n.callFrame,
          url,
          lineNumber,
          columnNumber,
          scriptId,
        },
      };
      cdpProfile.nodes.push(node);
    }

    const timeDeltas = event.args.data?.timeDeltas || [];
    const lines = event.args.data?.lines || Array(samples.length).fill(0);
    const columns = event.args.data?.columns || Array(samples.length).fill(0);
    cdpProfile.samples?.push(...samples);
    cdpProfile.timeDeltas?.push(...timeDeltas);
    cdpProfile.lines?.push(...lines);
    cdpProfile.columns?.push(...columns);

    if (traceIds) {
      cdpProfile.traceIds ??= {};
      for (const key in traceIds) {
        cdpProfile.traceIds[key] = traceIds[key];
      }
    }

    if (cdpProfile.samples && cdpProfile.timeDeltas && cdpProfile.samples.length !== cdpProfile.timeDeltas.length) {
      console.error('Failed to parse CPU profile.');
      return;
    }
    if (!cdpProfile.endTime && cdpProfile.timeDeltas) {
      const timeDeltas: number[] = cdpProfile.timeDeltas;
      cdpProfile.endTime = timeDeltas.reduce((x, y) => x + y, cdpProfile.startTime);
    }
    assignProfileSourceIfKnown(profileData, event.args?.data?.source);
    return;
  }
}

export async function finalize(parseOptions: Types.Configuration.ParseOptions = {}): Promise<void> {
  parseCPUProfileData(parseOptions);
}

function assignProfileSourceIfKnown(profileData: PreprocessedData, source: unknown): void {
  if (Types.Events.VALID_PROFILE_SOURCES.includes(source as Types.Events.ProfileSource)) {
    profileData.source = source as Types.Events.ProfileSource;
  }
}

export function data(): SamplesHandlerData {
  return {
    profilesInProcess,
    entryToNode,
  };
}

function getOrCreatePreProcessedData(
    processId: Types.Events.ProcessID, profileId: Types.Events.ProfileID): PreprocessedData {
  const profileById = Platform.MapUtilities.getWithDefault(preprocessedData, processId, () => new Map());
  return Platform.MapUtilities.getWithDefault<Types.Events.ProfileID, PreprocessedData>(
      profileById, profileId, () => ({
                                rawProfile: {
                                  startTime: 0,
                                  endTime: 0,
                                  nodes: [],
                                  samples: [],
                                  timeDeltas: [],
                                  lines: [],
                                  columns: [],
                                },
                                profileId,
                              }));
}

export interface SamplesHandlerData {
  profilesInProcess: typeof profilesInProcess;
  entryToNode: typeof entryToNode;
}

export interface ProfileData {
  profileId: Types.Events.ProfileID;
  rawProfile: CPUProfile.CPUProfileDataModel.ExtendedProfile;
  parsedProfile: CPUProfile.CPUProfileDataModel.CPUProfileDataModel;
  /**
   * Contains the calls built from the CPU profile samples.
   * Note: This doesn't contain real trace events coming from the
   * browser, only calls synthetically typed as trace events for
   * compatibility, as such it only makes sense to use them in pure CPU
   * profiles.
   *
   * If you need the profile calls from a CPU profile obtained from a
   * web trace, use the data exported by the RendererHandler instead.
   */
  profileCalls: Types.Events.SyntheticProfileCall[];
  /**
   * Contains the call tree built from the CPU profile samples.
   * Similar to the profileCalls field, this tree does not contain nor
   * take into account trace events, as such it only makes sense to use
   * them in pure CPU profiles.
   */
  profileTree?: Helpers.TreeHelpers.TraceEntryTree;
}

interface PreprocessedData {
  rawProfile: CPUProfile.CPUProfileDataModel.ExtendedProfile;
  profileId: Types.Events.ProfileID;
  threadId?: Types.Events.ThreadID;
  source?: Types.Events.ProfileSource;
}

/**
 * Returns the name of a function for a given synthetic profile call.
 * We first look to find the ProfileNode representing this call, and use its
 * function name. This is preferred (and should always exist) because if we
 * resolve sourcemaps, we will update this name. If that name is not present,
 * we fall back to the function name that was in the callframe that we got
 * when parsing the profile's trace data.
 */
export function getProfileCallFunctionName(data: SamplesHandlerData, entry: Types.Events.SyntheticProfileCall): string {
  const profile = data.profilesInProcess.get(entry.pid)?.get(entry.tid);
  const node = profile?.parsedProfile.nodeById(entry.nodeId);
  if (node?.functionName) {
    return node.functionName;
  }
  return entry.callFrame.functionName;
}
