// Copyright 2023 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 Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as Trace from '../../models/trace/trace.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';

import {
  addDecorationToEvent,
  buildGroupStyle,
  buildTrackHeader,
  getDurationString,
} from './AppenderUtils.js';
import {
  type CompatibilityTracksAppender,
  entryIsVisibleInTimeline,
  type PopoverInfo,
  type TrackAppender,
  type TrackAppenderName,
  VisualLoggingTrackName,
} from './CompatibilityTracksAppender.js';
import * as ModificationsManager from './ModificationsManager.js';
import * as Utils from './utils/utils.js';

const UIStrings = {
  /**
   * @description Text shown for an entry in the flame chart that is ignored because it matches
   * a predefined ignore list.
   * @example {/analytics\.js$} rule
   */
  onIgnoreList: 'On ignore list ({rule})',
  /**
   * @description Refers to the "Main frame", meaning the top level frame. See https://www.w3.org/TR/html401/present/frames.html
   * @example {example.com} PH1
   */
  mainS: 'Main — {PH1}',
  /**
   * @description Refers to the main thread of execution of a program. See https://developer.mozilla.org/en-US/docs/Glossary/Main_thread
   */
  main: 'Main',
  /**
   * @description Refers to any frame in the page. See https://www.w3.org/TR/html401/present/frames.html
   * @example {https://example.com} PH1
   */
  frameS: 'Frame — {PH1}',
  /**
   * @description A web worker in the page. See https://developer.mozilla.org/en-US/docs/Web/API/Worker
   * @example {https://google.com} PH1
   */
  workerS: '`Worker` — {PH1}',
  /**
   * @description A web worker in the page. See https://developer.mozilla.org/en-US/docs/Web/API/Worker
   * @example {FormatterWorker} PH1
   * @example {https://google.com} PH2
   */
  workerSS: '`Worker`: {PH1} — {PH2}',
  /**
   * @description Label for a web worker exclusively allocated for a purpose.
   */
  dedicatedWorker: 'Dedicated `Worker`',
  /**
   * @description A generic name given for a thread running in the browser (sequence of programmed instructions).
   * The placeholder is an enumeration given to the thread.
   * @example {1} PH1
   */
  threadS: 'Thread {PH1}',
  /**
   * @description Rasterization in computer graphics.
   */
  raster: 'Raster',
  /**
   * @description Threads used for background tasks.
   */
  threadPool: 'Thread pool',
  /**
   * @description Name for a thread that rasterizes graphics in a website.
   * @example {2} PH1
   */
  rasterizerThreadS: 'Rasterizer thread {PH1}',
  /**
   * @description Text in Timeline Flame Chart Data Provider of the Performance panel
   * @example {2} PH1
   */
  threadPoolThreadS: 'Thread pool worker {PH1}',
  /**
   * @description Title of a bidder auction worklet with known URL in the timeline flame chart of the Performance panel
   * @example {https://google.com} PH1
   */
  bidderWorkletS: 'Bidder Worklet — {PH1}',
  /**
   * @description Title of a bidder auction worklet in the timeline flame chart of the Performance panel with an unknown URL
   */
  bidderWorklet: 'Bidder Worklet',

  /**
   * @description Title of a seller auction worklet in the timeline flame chart of the Performance panel with an unknown URL
   */
  sellerWorklet: 'Seller Worklet',

  /**
   * @description Title of an auction worklet in the timeline flame chart of the Performance panel with an unknown URL
   */
  unknownWorklet: 'Auction Worklet',

  /**
   * @description Title of control thread of a service process for an auction worklet in the timeline flame chart of the Performance panel with an unknown URL
   */
  workletService: 'Auction Worklet service',

  /**
   * @description Title of a seller auction worklet with known URL in the timeline flame chart of the Performance panel
   * @example {https://google.com} PH1
   */
  sellerWorkletS: 'Seller Worklet — {PH1}',

  /**
   * @description Title of an auction worklet with known URL in the timeline flame chart of the Performance panel
   * @example {https://google.com} PH1
   */
  unknownWorkletS: 'Auction Worklet — {PH1}',

  /**
   * @description Title of control thread of a service process for an auction worklet with known URL in the timeline flame chart of the Performance panel
   * @example {https://google.com} PH1
   */
  workletServiceS: 'Auction Worklet service — {PH1}',
} as const;

const str_ = i18n.i18n.registerUIStrings('panels/timeline/ThreadAppender.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export class ThreadAppender implements TrackAppender {
  readonly appenderName: TrackAppenderName = 'Thread';

  #colorGenerator: Common.Color.Generator;
  #compatibilityBuilder: CompatibilityTracksAppender;
  #parsedTrace: Trace.TraceModel.ParsedTrace;

  #entries: readonly Trace.Types.Events.Event[] = [];
  #tree: Trace.Helpers.TreeHelpers.TraceEntryTree;
  #processId: Trace.Types.Events.ProcessID;
  #threadId: Trace.Types.Events.ThreadID;
  #threadDefaultName: string;
  #expanded = false;
  #headerAppended = false;
  readonly threadType: Trace.Handlers.Threads.ThreadType = Trace.Handlers.Threads.ThreadType.MAIN_THREAD;
  readonly isOnMainFrame: boolean;
  #showAllEventsEnabled = Common.Settings.Settings.instance().moduleSetting('timeline-show-all-events').get();
  #url = '';
  #headerNestingLevel: number|null = null;
  constructor(
      compatibilityBuilder: CompatibilityTracksAppender, parsedTrace: Trace.TraceModel.ParsedTrace,
      processId: Trace.Types.Events.ProcessID, threadId: Trace.Types.Events.ThreadID, threadName: string|null,
      type: Trace.Handlers.Threads.ThreadType, entries: readonly Trace.Types.Events.Event[],
      tree: Trace.Helpers.TreeHelpers.TraceEntryTree) {
    this.#compatibilityBuilder = compatibilityBuilder;
    // TODO(crbug.com/1456706):
    // The values for this color generator have been taken from the old
    // engine to keep the colors the same after the migration. This
    // generator is used here to create colors for js frames (profile
    // calls) in the flamechart by hashing the script's url. We might
    // need to reconsider this generator when migrating to GM3 colors.
    this.#colorGenerator =
        new Common.Color.Generator({min: 30, max: 330, count: undefined}, {min: 50, max: 80, count: 3}, 85);
    // Add a default color for call frames with no url.
    this.#colorGenerator.setColorForID('', '#f2ecdc');
    this.#parsedTrace = parsedTrace;
    this.#processId = processId;
    this.#threadId = threadId;

    if (!entries || !tree) {
      throw new Error(`Could not find data for thread with id ${threadId} in process with id ${processId}`);
    }
    this.#entries = entries;
    this.#tree = tree;
    this.#threadDefaultName = threadName || i18nString(UIStrings.threadS, {PH1: threadId});
    this.isOnMainFrame = Boolean(this.#parsedTrace.data.Renderer?.processes.get(processId)?.isOnMainFrame);
    this.threadType = type;
    // AuctionWorklets are threads, so we re-use this appender rather than
    // duplicate it, but we change the name because we want to render these
    // lower down than other threads.
    if (this.#parsedTrace.data.AuctionWorklets.worklets.has(processId)) {
      this.appenderName = 'Thread_AuctionWorklet';
    }
    this.#url = this.#parsedTrace.data.Renderer?.processes.get(this.#processId)?.url || '';
  }

  processId(): Trace.Types.Events.ProcessID {
    return this.#processId;
  }

  threadId(): Trace.Types.Events.ThreadID {
    return this.#threadId;
  }

  /**
   * Appends into the flame chart data the data corresponding to the
   * this thread.
   * @param trackStartLevel the horizontal level of the flame chart events where
   * the track's events will start being appended.
   * @param expanded whether the track should be rendered expanded.
   * @returns the first available level to append more data after having
   * appended the track's events.
   */
  appendTrackAtLevel(trackStartLevel: number, expanded = false): number {
    if (this.#entries.length === 0) {
      return trackStartLevel;
    }
    this.#expanded = expanded;
    return this.#appendTreeAtLevel(trackStartLevel);
  }

  setHeaderNestingLevel(level: number): void {
    this.#headerNestingLevel = level;
  }
  /**
   * Track header is appended only if there are events visible on it.
   * Otherwise we don't append any track. So, instead of preemptively
   * appending a track before appending its events, we only do so once
   * we have detected that the track contains an event that is visible.
   */
  #ensureTrackHeaderAppended(trackStartLevel: number): void {
    if (this.#headerAppended) {
      return;
    }
    if (this.threadType === Trace.Handlers.Threads.ThreadType.RASTERIZER ||
        this.threadType === Trace.Handlers.Threads.ThreadType.THREAD_POOL) {
      this.#appendGroupedTrackHeaderAndTitle(trackStartLevel, this.threadType);
    } else {
      this.#appendTrackHeaderAtLevel(trackStartLevel);
    }
    this.#headerAppended = true;
  }

  setHeaderAppended(headerAppended: boolean): void {
    this.#headerAppended = headerAppended;
  }

  headerAppended(): boolean {
    return this.#headerAppended;
  }

  /**
   * Adds into the flame chart data the header corresponding to this
   * thread. A header is added in the shape of a group in the flame
   * chart data. A group has a predefined style and a reference to the
   * definition of the legacy track (which should be removed in the
   * future).
   * @param currentLevel the flame chart level at which the header is
   * appended.
   */
  #appendTrackHeaderAtLevel(currentLevel: number): void {
    const trackIsCollapsible = this.#entries.length > 0;
    const style = buildGroupStyle({
      shareHeaderLine: false,
      collapsible: trackIsCollapsible ? PerfUI.FlameChart.GroupCollapsibleState.ALWAYS :
                                        PerfUI.FlameChart.GroupCollapsibleState.NEVER,
    });
    if (this.#headerNestingLevel !== null) {
      style.nestingLevel = this.#headerNestingLevel;
    }
    const visualLoggingName = this.#visualLoggingNameForThread();
    const group = buildTrackHeader(
        visualLoggingName, currentLevel, this.trackName(), style, /* selectable= */ true, this.#expanded,
        /* showStackContextMenu= */ true);
    this.#compatibilityBuilder.registerTrackForGroup(group, this);
  }

  #visualLoggingNameForThread(): VisualLoggingTrackName|null {
    switch (this.threadType) {
      case Trace.Handlers.Threads.ThreadType.MAIN_THREAD:
        return this.isOnMainFrame ? VisualLoggingTrackName.THREAD_MAIN : VisualLoggingTrackName.THREAD_FRAME;
      case Trace.Handlers.Threads.ThreadType.WORKER:
        return VisualLoggingTrackName.THREAD_WORKER;
      case Trace.Handlers.Threads.ThreadType.RASTERIZER:
        return VisualLoggingTrackName.THREAD_RASTERIZER;
      case Trace.Handlers.Threads.ThreadType.AUCTION_WORKLET:
        return VisualLoggingTrackName.THREAD_AUCTION_WORKLET;
      case Trace.Handlers.Threads.ThreadType.OTHER:
        return VisualLoggingTrackName.THREAD_OTHER;
      case Trace.Handlers.Threads.ThreadType.CPU_PROFILE:
        return VisualLoggingTrackName.THREAD_CPU_PROFILE;
      case Trace.Handlers.Threads.ThreadType.THREAD_POOL:
        return VisualLoggingTrackName.THREAD_POOL;
      default:
        return null;
    }
  }
  /**
   * Raster threads are rendered under a single header in the
   * flamechart. However, each thread has a unique title which needs to
   * be added to the flamechart data.
   */
  #appendGroupedTrackHeaderAndTitle(
      trackStartLevel: number,
      threadType: Trace.Handlers.Threads.ThreadType.RASTERIZER|Trace.Handlers.Threads.ThreadType.THREAD_POOL): void {
    const currentTrackCount = this.#compatibilityBuilder.getCurrentTrackCountForThreadType(threadType);
    if (currentTrackCount === 0) {
      const trackIsCollapsible = this.#entries.length > 0;
      const headerStyle = buildGroupStyle({
        shareHeaderLine: false,
        collapsible: trackIsCollapsible ? PerfUI.FlameChart.GroupCollapsibleState.ALWAYS :
                                          PerfUI.FlameChart.GroupCollapsibleState.NEVER,
      });

      // Don't set any jslogcontext (first argument) because this is a shared
      // header group. Each child will have its context set.
      const headerGroup = buildTrackHeader(
          null, trackStartLevel, this.trackName(), headerStyle, /* selectable= */ false, this.#expanded);
      this.#compatibilityBuilder.getFlameChartTimelineData().groups.push(headerGroup);
    }

    // Nesting is set to 1 because the track is appended inside the
    // header for all raster threads.
    const titleStyle =
        buildGroupStyle({padding: 2, nestingLevel: 1, collapsible: PerfUI.FlameChart.GroupCollapsibleState.NEVER});
    const rasterizerTitle = this.threadType === Trace.Handlers.Threads.ThreadType.RASTERIZER ?
        i18nString(UIStrings.rasterizerThreadS, {PH1: currentTrackCount + 1}) :
        i18nString(UIStrings.threadPoolThreadS, {PH1: currentTrackCount + 1});

    const visualLoggingName = this.#visualLoggingNameForThread();
    const titleGroup = buildTrackHeader(
        visualLoggingName, trackStartLevel, rasterizerTitle, titleStyle, /* selectable= */ true, this.#expanded);
    this.#compatibilityBuilder.registerTrackForGroup(titleGroup, this);
  }

  trackName(): string {
    let threadTypeLabel: string|null = null;
    switch (this.threadType) {
      case Trace.Handlers.Threads.ThreadType.MAIN_THREAD:
        threadTypeLabel = this.isOnMainFrame ? i18nString(UIStrings.mainS, {PH1: this.#url}) :
                                               i18nString(UIStrings.frameS, {PH1: this.#url});
        break;
      case Trace.Handlers.Threads.ThreadType.CPU_PROFILE:
        threadTypeLabel = i18nString(UIStrings.main);
        break;
      case Trace.Handlers.Threads.ThreadType.WORKER:
        threadTypeLabel = this.#buildNameForWorker();
        break;
      case Trace.Handlers.Threads.ThreadType.RASTERIZER:
        threadTypeLabel = i18nString(UIStrings.raster);
        break;
      case Trace.Handlers.Threads.ThreadType.THREAD_POOL:
        threadTypeLabel = i18nString(UIStrings.threadPool);
        break;
      case Trace.Handlers.Threads.ThreadType.OTHER:
        break;
      case Trace.Handlers.Threads.ThreadType.AUCTION_WORKLET:
        threadTypeLabel = this.#buildNameForAuctionWorklet();
        break;
      default:
        return Platform.assertNever(this.threadType, `Unknown thread type: ${this.threadType}`);
    }
    let suffix = '';
    if (this.#parsedTrace.data.Meta.traceIsGeneric) {
      suffix = suffix + ` (${this.threadId()})`;
    }
    return (threadTypeLabel || this.#threadDefaultName) + suffix;
  }

  getUrl(): string {
    return this.#url;
  }

  getEntries(): readonly Trace.Types.Events.Event[] {
    return this.#entries;
  }

  #buildNameForAuctionWorklet(): string {
    const workletMetadataEvent = this.#parsedTrace.data.AuctionWorklets.worklets.get(this.#processId);
    // We should always have this event - if we do not, we were instantiated with invalid data.
    if (!workletMetadataEvent) {
      return i18nString(UIStrings.unknownWorklet);
    }

    // Host could be empty - in which case we do not want to add it.
    const host = workletMetadataEvent.host ? `https://${workletMetadataEvent.host}` : '';
    const shouldAddHost = host.length > 0;

    // For each Auction Worklet in a page there are two threads we care about on the same process.
    // 1. The "Worklet Service" which is a generic helper service. This thread
    // is always named "auction_worklet.CrUtilityMain".
    //
    // 2. The "Seller/Bidder" service. This thread is always named
    // "AuctionV8HelperThread". The AuctionWorkets handler does the job of
    // figuring this out for us - the metadata event it provides for each
    // worklet process will have a `type` already set.
    //
    // Therefore, for this given thread, which we know is part of
    // an AuctionWorklet process, we need to figure out if this thread is the
    // generic service, or a seller/bidder worklet.
    //
    // Note that the worklet could also have the "unknown" type - this is not
    // expected but implemented to prevent trace event changes causing DevTools
    // to break with unknown worklet types.
    const isUtilityThread = workletMetadataEvent.args.data.utilityThread.tid === this.#threadId;
    const isBidderOrSeller = workletMetadataEvent.args.data.v8HelperThread.tid === this.#threadId;

    if (isUtilityThread) {
      return shouldAddHost ? i18nString(UIStrings.workletServiceS, {PH1: host}) : i18nString(UIStrings.workletService);
    }

    if (isBidderOrSeller) {
      switch (workletMetadataEvent.type) {
        case Trace.Types.Events.AuctionWorkletType.SELLER:
          return shouldAddHost ? i18nString(UIStrings.sellerWorkletS, {PH1: host}) :
                                 i18nString(UIStrings.sellerWorklet);
        case Trace.Types.Events.AuctionWorkletType.BIDDER:
          return shouldAddHost ? i18nString(UIStrings.bidderWorkletS, {PH1: host}) :
                                 i18nString(UIStrings.bidderWorklet);
        case Trace.Types.Events.AuctionWorkletType.UNKNOWN:
          return shouldAddHost ? i18nString(UIStrings.unknownWorkletS, {PH1: host}) :
                                 i18nString(UIStrings.unknownWorklet);
        default:
          Platform.assertNever(
              workletMetadataEvent.type, `Unexpected Auction Worklet Type ${workletMetadataEvent.type}`);
      }
    }
    // We should never reach here, but just in case!
    return shouldAddHost ? i18nString(UIStrings.unknownWorkletS, {PH1: host}) : i18nString(UIStrings.unknownWorklet);
  }

  #buildNameForWorker(): string {
    const url = this.#parsedTrace.data.Renderer?.processes.get(this.#processId)?.url || '';
    const workerId = this.#parsedTrace.data.Workers.workerIdByThread.get(this.#threadId);
    const workerURL = workerId ? this.#parsedTrace.data.Workers.workerURLById.get(workerId) : url;
    // Try to create a name using the worker url if present. If not, use a generic label.
    let workerName =
        workerURL ? i18nString(UIStrings.workerS, {PH1: workerURL}) : i18nString(UIStrings.dedicatedWorker);
    const workerTarget = workerId !== undefined && SDK.TargetManager.TargetManager.instance().targetById(workerId);
    if (workerTarget) {
      // Get the worker name from the target, which corresponds to the name
      // assigned to the worker when it was constructed.
      workerName = i18nString(UIStrings.workerSS, {PH1: workerTarget.name(), PH2: url});
    }
    return workerName;
  }

  /**
   * Adds into the flame chart data the entries of this thread, which
   * includes trace events and JS calls.
   * @param currentLevel the flame chart level from which entries will
   * be appended.
   * @returns the next level after the last occupied by the appended
   * entries (the first available level to append more data).
   */
  #appendTreeAtLevel(trackStartLevel: number): number {
    // We can not used the tree maxDepth in the tree from the
    // RendererHandler because ignore listing and visibility of events
    // alter the final depth of the flame chart.
    return this.#appendNodesAtLevel(this.#tree.roots, trackStartLevel);
  }

  /**
   * Traverses the trees formed by the provided nodes in breadth first
   * fashion and appends each node's entry on each iteration. As each
   * entry is handled, a check for the its visibility or if it's ignore
   * listed is done before appending.
   */
  #appendNodesAtLevel(
      nodes: Iterable<Trace.Helpers.TreeHelpers.TraceEntryNode>, startingLevel: number,
      parentIsIgnoredListed = false): number {
    const invisibleEntries =
        ModificationsManager.ModificationsManager.activeManager()?.getEntriesFilter().invisibleEntries() ?? [];
    let maxDepthInTree = startingLevel;
    for (const node of nodes) {
      let nextLevel = startingLevel;
      const entry = node.entry;
      const entryIsIgnoreListed = Utils.IgnoreList.isIgnoreListedEntry(entry);
      // Events' visibility is determined from their predefined styles,
      // which is something that's not available in the engine data.
      // Thus it needs to be checked in the appenders, but preemptively
      // checking if there are visible events and returning early if not
      // is potentially expensive since, in theory, we would be adding
      // another traversal to the entries array (which could grow
      // large). To avoid the extra cost we  add the check in the
      // traversal we already need to append events.
      const entryIsVisible = !invisibleEntries.includes(entry) &&
          (entryIsVisibleInTimeline(entry, this.#parsedTrace) || this.#showAllEventsEnabled);
      // For ignore listing support, these two conditions need to be met
      // to not append a profile call to the flame chart:
      // 1. It is ignore listed
      // 2. It is NOT the bottom-most call in an ignore listed stack (a
      //    set of chained profile calls that belong to ignore listed
      //    URLs).
      // This means that all of the ignore listed calls are ignored (not
      // appended), except if it is the bottom call of an ignored stack.
      // This is because to represent ignore listed stack frames, we add
      // a flame chart entry with the length and position of the bottom
      // frame, which is distinctively marked to denote an ignored listed
      // stack.
      const skipEventDueToIgnoreListing = entryIsIgnoreListed && parentIsIgnoredListed;
      if (entryIsVisible && !skipEventDueToIgnoreListing) {
        this.#appendEntryAtLevel(entry, startingLevel);
        nextLevel++;
      }

      const depthInChildTree = this.#appendNodesAtLevel(node.children, nextLevel, entryIsIgnoreListed);
      maxDepthInTree = Math.max(depthInChildTree, maxDepthInTree);
    }
    return maxDepthInTree;
  }

  #appendEntryAtLevel(entry: Trace.Types.Events.Event, level: number): void {
    this.#ensureTrackHeaderAppended(level);
    const index = this.#compatibilityBuilder.appendEventAtLevel(entry, level, this);
    this.#addDecorationsToEntry(entry, index);
  }

  #addDecorationsToEntry(entry: Trace.Types.Events.Event, index: number): void {
    const flameChartData = this.#compatibilityBuilder.getFlameChartTimelineData();
    if (ModificationsManager.ModificationsManager.activeManager()?.getEntriesFilter().isEntryExpandable(entry)) {
      addDecorationToEvent(
          flameChartData, index, {type: PerfUI.FlameChart.FlameChartDecorationType.HIDDEN_DESCENDANTS_ARROW});
    }
    const warnings = this.#parsedTrace.data.Warnings.perEvent.get(entry);
    if (!warnings) {
      return;
    }
    addDecorationToEvent(flameChartData, index, {type: PerfUI.FlameChart.FlameChartDecorationType.WARNING_TRIANGLE});
    if (!warnings.includes('LONG_TASK')) {
      return;
    }
    addDecorationToEvent(flameChartData, index, {
      type: PerfUI.FlameChart.FlameChartDecorationType.CANDY,
      startAtTime: Trace.Handlers.ModelHandlers.Warnings.LONG_MAIN_THREAD_TASK_THRESHOLD,
    });
  }

  /*
    ------------------------------------------------------------------------------------
     The following methods  are invoked by the flame chart renderer to query features about
     events on rendering.
    ------------------------------------------------------------------------------------
  */

  /**
   * Gets the color an event added by this appender should be rendered with.
   */
  colorForEvent(event: Trace.Types.Events.Event): string {
    if (this.#parsedTrace.data.Meta.traceIsGeneric) {
      return event.name ? `hsl(${Platform.StringUtilities.hashCode(event.name) % 300 + 30}, 40%, 70%)` : '#ccc';
    }

    if (Trace.Types.Events.isProfileCall(event)) {
      if (event.callFrame.functionName === '(idle)') {
        return categoryColorValue(Trace.Styles.getCategoryStyles().idle);
      }
      if (event.callFrame.functionName === '(program)') {
        return categoryColorValue(Trace.Styles.getCategoryStyles().other);
      }
      if (event.callFrame.scriptId === '0') {
        // If we can not match this frame to a script, return the
        // generic "scripting" color.
        return categoryColorValue(Trace.Styles.getCategoryStyles().scripting);
      }
      // Otherwise, return a color created based on its URL.
      return this.#colorGenerator.colorForID(event.callFrame.url);
    }
    const eventStyles = Trace.Styles.getEventStyle(event.name as Trace.Types.Events.Name);
    if (eventStyles) {
      return categoryColorValue(eventStyles.category);
    }

    return categoryColorValue(Trace.Styles.getCategoryStyles().other);
  }

  /**
   * Gets the title an event added by this appender should be rendered with.
   */
  titleForEvent(entry: Trace.Types.Events.Event): string {
    if (Utils.IgnoreList.isIgnoreListedEntry(entry)) {
      const rule = Utils.IgnoreList.getIgnoredReasonString(entry);
      return i18nString(UIStrings.onIgnoreList, {rule});
    }
    return Trace.Name.forEntry(entry, this.#parsedTrace);
  }

  setPopoverInfo(event: Trace.Types.Events.Event, info: PopoverInfo): void {
    if (Trace.Types.Events.isParseHTML(event)) {
      const startLine = event.args['beginData']['startLine'];
      const endLine = event.args['endData']?.['endLine'];
      const eventURL = event.args['beginData']['url'] as Platform.DevToolsPath.UrlString;
      const url = Bindings.ResourceUtils.displayNameForURL(eventURL);
      const range = (endLine !== -1 || endLine === startLine) ? `${startLine}...${endLine}` : startLine;
      info.title += ` - ${url} [${range}]`;
    }
    const selfTime = this.#parsedTrace.data.Renderer.entryToNode.get(event)?.selfTime;
    info.formattedTime = getDurationString(event.dur, selfTime);
  }
}

function categoryColorValue(category: Trace.Styles.TimelineCategory): string {
  return ThemeSupport.ThemeSupport.instance().getComputedValue(category.cssVariable);
}
