// 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 * as Helpers from '../helpers/helpers.js';
import * as Types from '../types/types.js';

import type {HandlerName} from './types.js';
import {data as userTimingsData} from './UserTimingsHandler.js';

let extensionTrackEntries: Types.Extensions.SyntheticExtensionTrackEntry[] = [];
let extensionTrackData: Types.Extensions.ExtensionTrackData[] = [];
let extensionMarkers: Types.Extensions.SyntheticExtensionMarker[] = [];
let entryToNode = new Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>();
let timeStampByName = new Map<string, Types.Events.ConsoleTimeStamp>();

let syntheticConsoleEntriesForTimingsTrack: Types.Events.SyntheticConsoleTimeStamp[] = [];

export interface ExtensionTraceData {
  extensionTrackData: readonly Types.Extensions.ExtensionTrackData[];
  extensionMarkers: readonly Types.Extensions.SyntheticExtensionMarker[];
  // TODO(andoli): Can we augment Renderer's entryToNode instead? To avoid the split of TimelineUIUtils's getEventSelfTime()?
  entryToNode: Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>;
  syntheticConsoleEntriesForTimingsTrack: Types.Events.SyntheticConsoleTimeStamp[];
}

export function handleEvent(_event: Types.Events.Event): void {
  // Implementation not needed because data is sourced from UserTimingsHandler
}

export function reset(): void {
  extensionTrackEntries = [];
  syntheticConsoleEntriesForTimingsTrack = [];
  extensionTrackData = [];
  extensionMarkers = [];
  entryToNode = new Map();
  timeStampByName = new Map();
}

export async function finalize(): Promise<void> {
  createExtensionFlameChartEntries();
}

function createExtensionFlameChartEntries(): void {
  const pairedMeasures: readonly Types.Events.SyntheticUserTimingPair[] = userTimingsData().performanceMeasures;
  const marks: readonly Types.Events.PerformanceMark[] = userTimingsData().performanceMarks;
  const mergedRawExtensionEvents = Helpers.Trace.mergeEventsInOrder(pairedMeasures, marks);

  extractPerformanceAPIExtensionEntries(mergedRawExtensionEvents);
  extractConsoleAPIExtensionEntries();
  // extensionTrackEntries is filled by the above two calls.
  Helpers.Trace.sortTraceEventsInPlace(extensionTrackEntries);
  Helpers.Extensions.buildTrackDataFromExtensionEntries(extensionTrackEntries, extensionTrackData, entryToNode);
}

/**
 * Extracts extension entries from console.timeStamp events.
 *
 * Entries are built by pairing `console.timeStamp` events based on
 * their names. When a `console.timeStamp` event includes a `start`
 * argument (and optionally an `end` argument), it attempts to find
 * previously recorded `console.timeStamp` events with names matching
 * the `start` and `end` values. These matching events are then used to
 * determine the start and end times of the new entry.
 *
 * If a `console.timeStamp` event includes data for a custom track
 * (specified by the `track` argument), an extension track entry is
 * created and added to the `extensionTrackEntries` array. These entries
 * are used to visualize custom tracks in the Performance panel.
 *
 * If a `console.timeStamp` event includes data for a custom track
 * (specified by the `track` argument), an extension track entry is
 * created and added to the `extensionTrackEntries` array. These entries
 * are used to visualize custom tracks in the Performance panel.
 *
 * If a `console.timeStamp` event does not specify a custom track but
 * includes a start and/or end time (referencing other
 * `console.timeStamp` names), a synthetic console time stamp entry is
 * created and added to the `syntheticConsoleEntriesForTimingsTrack`
 * array. These entries are displayed in the "Timings" track.
 */
export function extractConsoleAPIExtensionEntries(): void {
  const consoleTimeStamps: readonly Types.Events.ConsoleTimeStamp[] = userTimingsData().timestampEvents;
  for (const currentTimeStamp of consoleTimeStamps) {
    if (!currentTimeStamp.args.data) {
      continue;
    }
    const timeStampName = String(currentTimeStamp.args.data.name ?? currentTimeStamp.args.data.message);
    timeStampByName.set(timeStampName, currentTimeStamp);
    const {devtoolsObj: extensionData, userDetail} = extensionDataInConsoleTimeStamp(currentTimeStamp);
    const start = currentTimeStamp.args.data.start;
    const end = currentTimeStamp.args.data.end;
    if (!extensionData && !start && !end) {
      continue;
    }
    // If the start or end is a number, it's assumed to be a timestamp
    // from the tracing clock, so we use that directly, otherwise we
    // assume it's the label of a previous console timestamp, in which
    // case we use its corresponding timestamp.
    const startTimeStamp =
        typeof start === 'number' ? Types.Timing.Micro(start) : timeStampByName.get(String(start))?.ts;
    const endTimeStamp = typeof end === 'number' ? Types.Timing.Micro(end) : timeStampByName.get(String(end))?.ts;
    if (endTimeStamp !== undefined && startTimeStamp === undefined) {
      // Invalid data
      continue;
    }
    const entryStartTime = startTimeStamp ?? currentTimeStamp.ts;
    const entryEndTime = endTimeStamp ?? currentTimeStamp.ts;
    if (extensionData) {
      const unregisteredExtensionEntry: Omit<Types.Extensions.SyntheticExtensionTrackEntry, '_tag'> = {
        ...currentTimeStamp,
        name: timeStampName,
        cat: 'devtools.extension',
        devtoolsObj: extensionData,
        userDetail,
        rawSourceEvent: currentTimeStamp,
        dur: Types.Timing.Micro(entryEndTime - entryStartTime),
        ts: entryStartTime,
        ph: Types.Events.Phase.COMPLETE,
      };
      const extensionEntry =
          Helpers.SyntheticEvents.SyntheticEventsManager
              .registerSyntheticEvent<Types.Extensions.SyntheticExtensionTrackEntry>(unregisteredExtensionEntry);
      extensionTrackEntries.push(extensionEntry);
      continue;
    }
    // If no extension data is found in the entry (no custom track name
    // was passed), but the entry has a duration. we still save it here
    // to be added in the timings track. Note that timings w/o duration
    // and extension data are already handled by the UserTimingsHandler.
    const unregisteredSyntheticTimeStamp: Omit<Types.Events.SyntheticConsoleTimeStamp, '_tag'> = {
      ...currentTimeStamp,
      name: timeStampName,
      cat: 'disabled-by-default-v8.inspector',
      ph: Types.Events.Phase.COMPLETE,
      ts: entryStartTime,
      dur: Types.Timing.Micro(entryEndTime - entryStartTime),
      rawSourceEvent: currentTimeStamp
    };
    const syntheticTimeStamp =
        Helpers.SyntheticEvents.SyntheticEventsManager.registerSyntheticEvent<Types.Events.SyntheticConsoleTimeStamp>(
            unregisteredSyntheticTimeStamp);
    syntheticConsoleEntriesForTimingsTrack.push(syntheticTimeStamp);
  }
}

/**
 * Extracts extension entries from Performance API events (marks and
 * measures).
 * It specifically looks for events that contain extension-specific data
 * within their `detail` property.
 *
 * If an event's `detail` property can be parsed as a JSON object and
 * contains a `devtools` field with a valid extension payload, a
 * synthetic extension entry is created. The type of extension entry
 * created depends on the payload:
 *
 * - If the payload conforms to `ExtensionPayloadMarker`, a
 *   `SyntheticExtensionMarker` is created and added to the
 *   `extensionMarkers` array. These markers represent single points in
 *   time.
 * - If the payload conforms to `ExtensionPayloadTrackEntry`, a
 *   `SyntheticExtensionTrackEntry` is created and added to the
 *   `extensionTrackEntries` array. These entries represent events with
 *   a duration and are displayed on custom tracks in the Performance
 *   panel.
 *
 * **Note:** Only events with a `detail` property that contains valid
 * extension data are processed. Other `performance.mark` and
 * `performance.measure` events are ignored.
 *
 * @param timings An array of `SyntheticUserTimingPair` or
 *                `PerformanceMark` events, typically obtained from the
 *                `UserTimingsHandler`.
 */
export function extractPerformanceAPIExtensionEntries(
    timings: Array<Types.Events.SyntheticUserTimingPair|Types.Events.PerformanceMark>): void {
  for (const timing of timings) {
    const {devtoolsObj, userDetail} = extensionDataInPerformanceTiming(timing);
    if (!devtoolsObj) {
      // Not an extension user timing.
      continue;
    }

    const extensionSyntheticEntry = {
      name: timing.name,
      ph: Types.Extensions.isExtensionPayloadMarker(devtoolsObj) ? Types.Events.Phase.INSTANT :
                                                                   Types.Events.Phase.COMPLETE,
      pid: timing.pid,
      tid: timing.tid,
      ts: timing.ts,
      dur: timing.dur as Types.Timing.Micro,
      cat: 'devtools.extension',
      devtoolsObj,
      userDetail,
      rawSourceEvent: Types.Events.isSyntheticUserTiming(timing) ? timing.rawSourceEvent : timing,
    };

    if (Types.Extensions.isExtensionPayloadMarker(devtoolsObj)) {
      const extensionMarker =
          Helpers.SyntheticEvents.SyntheticEventsManager
              .registerSyntheticEvent<Types.Extensions.SyntheticExtensionMarker>(
                  extensionSyntheticEntry as Omit<Types.Extensions.SyntheticExtensionMarker, '_tag'>);
      extensionMarkers.push(extensionMarker);
      continue;
    }

    if (Types.Extensions.isExtensionEntryObj(extensionSyntheticEntry.devtoolsObj)) {
      const extensionTrackEntry =
          Helpers.SyntheticEvents.SyntheticEventsManager
              .registerSyntheticEvent<Types.Extensions.SyntheticExtensionTrackEntry>(
                  extensionSyntheticEntry as Omit<Types.Extensions.SyntheticExtensionTrackEntry, '_tag'>);
      extensionTrackEntries.push(extensionTrackEntry);
      continue;
    }
  }
}

/**
 * Parses out the data in a performance.measure / mark call into two parts:
 * 1. devtoolsObj: this is the data required to be passed by the user for the
 *    event to be used to create a custom track in the performance panel.
 * 2. userDetail: this is arbitrary data the user has attached to the event
 *    that we show in the summary drawer.
 */
export function extensionDataInPerformanceTiming(
    timing: Types.Events.SyntheticUserTimingPair|Types.Events.PerformanceMark):
    {devtoolsObj: Types.Extensions.DevToolsObj|null, userDetail: Types.Extensions.JsonValue|null} {
  const timingDetail =
      Types.Events.isPerformanceMark(timing) ? timing.args.data?.detail : timing.args.data.beginEvent.args.detail;
  if (!timingDetail) {
    return {devtoolsObj: null, userDetail: null};
  }
  const devtoolsObj = Helpers.Trace.parseDevtoolsDetails(timingDetail, 'devtools') as Types.Extensions.DevToolsObj;

  let userDetail = null;
  try {
    userDetail = JSON.parse(timingDetail);
    delete userDetail.devtools;
  } catch {
    // Nothing to do here, we still want to return the `devtools` part to make
    // this a custom event, even if the user detail failed to parse.
  }

  return {devtoolsObj, userDetail};
}

/**
 * Extracts extension data from a `console.timeStamp` event.
 *
 * Checks if a `console.timeStamp` event contains data intended for
 * creating a custom track entry in the DevTools Performance panel. It
 * specifically looks for a `track` argument within the event's data.
 *
 * If a `track` argument is present (and not an empty string), the
 * function constructs an `ExtensionTrackEntryPayload` object containing
 * the track name, an optional color, an optional track group. This
 * payload is then used to create a `SyntheticExtensionTrackEntry`.
 *
 * **Note:** The `color` argument is optional and its type is validated
 * against a predefined palette (see
 * `ExtensionUI::extensionEntryColor`).
 *
 * @param timeStamp The `ConsoleTimeStamp` event to extract data from.
 * @returns An `ExtensionTrackEntryPayload` object if the event contains
 *         valid extension data for a track entry, or `null` otherwise.
 */
export function extensionDataInConsoleTimeStamp(timeStamp: Types.Events.ConsoleTimeStamp):
    {devtoolsObj: Types.Extensions.DevToolsObjEntry|null, userDetail: Types.Extensions.JsonValue|null} {
  if (!timeStamp.args.data || !timeStamp.args.data.track) {
    return {devtoolsObj: null, userDetail: null};
  }

  let userDetail = null;
  try {
    // While it's in the trace as 'devtools', it's just the 7th argument to console.timeStamp(), stringified.
    // If no data, fall back to falsy empty string.
    userDetail = JSON.parse(timeStamp.args.data?.devtools || '""') as Types.Extensions.JsonValue;
  } catch {
  }

  const devtoolsObj: Types.Extensions.DevToolsObjEntry = {
    // the color is defaulted to primary if it's value isn't one from
    // the defined palette (see ExtensionUI::extensionEntryColor) so
    // we don't need to check the value is valid here.
    color: String(timeStamp.args.data.color) as Types.Extensions.DevToolsObjEntry['color'],
    track: String(timeStamp.args.data.track),
    dataType: 'track-entry',
    trackGroup: timeStamp.args.data.trackGroup !== undefined ? String(timeStamp.args.data.trackGroup) : undefined,
  };

  return {devtoolsObj, userDetail};
}

export function data(): ExtensionTraceData {
  return {
    entryToNode,
    extensionTrackData,
    extensionMarkers,
    syntheticConsoleEntriesForTimingsTrack,
  };
}

export function deps(): HandlerName[] {
  return ['UserTimings'];
}
