// 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 i18n from '../../../core/i18n/i18n.js';
import type * as Handlers from '../handlers/handlers.js';
import type {SelectorStatsData} from '../handlers/SelectorStatsHandler.js';
import * as Helpers from '../helpers/helpers.js';
import {type SelectorTiming, SelectorTimingsKey} from '../types/TraceEvents.js';
import * as Types from '../types/types.js';

import {
  InsightCategory,
  InsightKeys,
  type InsightModel,
  type InsightSetContext,
  type PartialInsightModel,
} from './types.js';

export const UIStrings = {
  /**
   * @description Title of an insight that provides details about slow CSS selectors.
   */
  title: 'CSS Selector costs',

  /**
   * @description Text to describe how to improve the performance of CSS selectors.
   */
  description:
      'If Recalculate Style costs remain high, selector optimization can reduce them. [Optimize the selectors](https://developer.chrome.com/docs/performance/insights/slow-css-selector) with both high elapsed time and high slow-path %. Simpler selectors, fewer selectors, a smaller DOM, and a shallower DOM will all reduce matching costs.',
  /**
   * @description Column name for count of elements that the engine attempted to match against a style rule
   */
  matchAttempts: 'Match attempts',
  /**
   * @description Column name for count of elements that matched a style rule
   */
  matchCount: 'Match count',
  /**
   * @description Column name for elapsed time spent computing a style rule
   */
  elapsed: 'Elapsed time',
  /**
   * @description Column name for the selectors that took the longest amount of time/effort.
   */
  topSelectors: 'Top selectors',
  /**
   * @description Column name for a total sum.
   */
  total: 'Total',
  /**
   * @description Text status indicating that no CSS selector data was found.
   */
  enableSelectorData:
      'No CSS selector data was found. CSS selector stats need to be enabled in the performance panel settings.',
  /**
   * @description top CSS selector when ranked by elapsed time in ms
   */
  topSelectorElapsedTime: 'Top selector elapsed time',
  /**
   * @description top CSS selector when ranked by match attempt
   */
  topSelectorMatchAttempt: 'Top selector match attempt',
} as const;

const str_ = i18n.i18n.registerUIStrings('models/trace/insights/SlowCSSSelector.ts', UIStrings);
export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const slowCSSSelectorThreshold = 500;  // 500us threshold for slow selectors

export type SlowCSSSelectorInsightModel = InsightModel<typeof UIStrings, {
  totalElapsedMs: Types.Timing.Milli,
  totalMatchAttempts: number,
  totalMatchCount: number,
  topSelectorElapsedMs: Types.Events.SelectorTiming | null,
  topSelectorMatchAttempts: Types.Events.SelectorTiming | null,
}>;

function aggregateSelectorStats(data: SelectorStatsData, context: InsightSetContext): SelectorTiming[] {
  const selectorMap = new Map<String, SelectorTiming>();

  for (const [event, value] of data.dataForRecalcStyleEvent) {
    if (event.args.beginData?.frame !== context.frameId) {
      continue;
    }
    if (!Helpers.Timing.eventIsInBounds(event, context.bounds)) {
      continue;
    }
    for (const timing of value.timings) {
      const key = timing[SelectorTimingsKey.Selector] + '_' + timing[SelectorTimingsKey.StyleSheetId];
      const findTiming = selectorMap.get(key);
      if (findTiming !== undefined) {
        findTiming[SelectorTimingsKey.Elapsed] += timing[SelectorTimingsKey.Elapsed];
        findTiming[SelectorTimingsKey.FastRejectCount] += timing[SelectorTimingsKey.FastRejectCount];
        findTiming[SelectorTimingsKey.MatchAttempts] += timing[SelectorTimingsKey.MatchAttempts];
        findTiming[SelectorTimingsKey.MatchCount] += timing[SelectorTimingsKey.MatchCount];
      } else {
        selectorMap.set(key, {...timing});
      }
    }
  }

  return [...selectorMap.values()];
}

function finalize(partialModel: PartialInsightModel<SlowCSSSelectorInsightModel>): SlowCSSSelectorInsightModel {
  return {
    insightKey: InsightKeys.SLOW_CSS_SELECTOR,
    strings: UIStrings,
    title: i18nString(UIStrings.title),
    description: i18nString(UIStrings.description),
    docs: 'https://developer.chrome.com/docs/performance/insights/slow-css-selector',
    category: InsightCategory.ALL,
    state: partialModel.topSelectorElapsedMs && partialModel.topSelectorMatchAttempts ? 'informative' : 'pass',
    ...partialModel,
  };
}

export function isSlowCSSSelectorInsight(model: InsightModel): model is SlowCSSSelectorInsightModel {
  return model.insightKey === InsightKeys.SLOW_CSS_SELECTOR;
}

export function generateInsight(
    data: Handlers.Types.HandlerData, context: InsightSetContext): SlowCSSSelectorInsightModel {
  const selectorStatsData = data.SelectorStats;

  if (!selectorStatsData) {
    throw new Error('no selector stats data');
  }

  const selectorTimings = aggregateSelectorStats(selectorStatsData, context);

  let totalElapsedUs = 0;
  let totalMatchAttempts = 0;
  let totalMatchCount = 0;

  selectorTimings.map(timing => {
    totalElapsedUs += timing[SelectorTimingsKey.Elapsed];
    totalMatchAttempts += timing[SelectorTimingsKey.MatchAttempts];
    totalMatchCount += timing[SelectorTimingsKey.MatchCount];
  });

  let topSelectorElapsedMs: SelectorTiming|null = null;
  let topSelectorMatchAttempts: SelectorTiming|null = null;

  if (selectorTimings.length > 0) {
    // find the selector with most elapsed time
    topSelectorElapsedMs = selectorTimings.reduce((a, b) => {
      return a[SelectorTimingsKey.Elapsed] > b[SelectorTimingsKey.Elapsed] ? a : b;
    });

    // check if the slowest selector is slow enough to trigger insights info
    if (topSelectorElapsedMs && topSelectorElapsedMs[SelectorTimingsKey.Elapsed] < slowCSSSelectorThreshold) {
      topSelectorElapsedMs = null;
    }

    // find the selector with most match attempts
    topSelectorMatchAttempts = selectorTimings.reduce((a, b) => {
      return a[SelectorTimingsKey.MatchAttempts] > b[SelectorTimingsKey.MatchAttempts] ? a : b;
    });
  }

  return finalize({
    // TODO: should we identify RecalcStyle events as linked to this insight?
    relatedEvents: [],
    totalElapsedMs: Types.Timing.Milli(totalElapsedUs / 1000.0),
    totalMatchAttempts,
    totalMatchCount,
    topSelectorElapsedMs,
    topSelectorMatchAttempts,
  });
}

export function createOverlays(_: SlowCSSSelectorInsightModel): Types.Overlays.Overlay[] {
  return [];
}
