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

import * as ThirdPartyWeb from '../../../third_party/third-party-web/third-party-web.js';
import * as Handlers from '../handlers/handlers.js';
import * as Helpers from '../helpers/helpers.js';
import * as Types from '../types/types.js';

export type Entity = typeof ThirdPartyWeb.ThirdPartyWeb.entities[number];

export interface Summary {
  transferSize: number;
  mainThreadTime: Types.Timing.Micro;
}

export interface ThirdPartySummary {
  byEntity: Map<Entity, Summary>;
  byUrl: Map<string, Summary>;
  urlsByEntity: Map<Entity, Set<string>>;
  eventsByEntity: Map<Entity, Types.Events.Event[]>;
  madeUpEntityCache: Map<string, Entity>;
}

function getOrMakeSummaryByEntity(
    thirdPartySummary: ThirdPartySummary, event: Types.Events.Event, url: string): Summary|null {
  const entity = ThirdPartyWeb.ThirdPartyWeb.getEntity(url) ??
      Handlers.Helpers.makeUpEntity(thirdPartySummary.madeUpEntityCache, url);
  if (!entity) {
    return null;
  }

  const urls = thirdPartySummary.urlsByEntity.get(entity) ?? new Set();
  urls.add(url);
  thirdPartySummary.urlsByEntity.set(entity, urls);

  const events = thirdPartySummary.eventsByEntity.get(entity) ?? [];
  events.push(event);
  thirdPartySummary.eventsByEntity.set(entity, events);

  let summary = thirdPartySummary.byEntity.get(entity);
  if (summary) {
    return summary;
  }

  summary = {transferSize: 0, mainThreadTime: Types.Timing.Micro(0)};
  thirdPartySummary.byEntity.set(entity, summary);
  return summary;
}

function getOrMakeSummaryByURL(thirdPartySummary: ThirdPartySummary, url: string): Summary|null {
  let summary = thirdPartySummary.byUrl.get(url);
  if (summary) {
    return summary;
  }

  summary = {transferSize: 0, mainThreadTime: Types.Timing.Micro(0)};
  thirdPartySummary.byUrl.set(url, summary);
  return summary;
}

function collectMainThreadActivity(
    thirdPartySummary: ThirdPartySummary, parsedTrace: Handlers.Types.ParsedTrace,
    bounds: Types.Timing.TraceWindowMicro): void {
  for (const process of parsedTrace.Renderer.processes.values()) {
    if (!process.isOnMainFrame) {
      continue;
    }

    for (const thread of process.threads.values()) {
      if (thread.name === 'CrRendererMain') {
        if (!thread.tree) {
          break;
        }

        for (const event of thread.entries) {
          if (!Helpers.Timing.eventIsInBounds(event, bounds)) {
            continue;
          }

          const node = parsedTrace.Renderer.entryToNode.get(event);
          if (!node || !node.selfTime) {
            continue;
          }

          const url = Handlers.Helpers.getNonResolvedURL(event, parsedTrace as Handlers.Types.ParsedTrace);
          if (!url) {
            continue;
          }

          let summary = getOrMakeSummaryByEntity(thirdPartySummary, event, url);
          if (summary) {
            summary.mainThreadTime = (summary.mainThreadTime + node.selfTime) as Types.Timing.Micro;
          }

          summary = getOrMakeSummaryByURL(thirdPartySummary, url);
          if (summary) {
            summary.mainThreadTime = (summary.mainThreadTime + node.selfTime) as Types.Timing.Micro;
          }
        }
      }
    }
  }
}

function collectNetworkActivity(
    thirdPartySummary: ThirdPartySummary, requests: Types.Events.SyntheticNetworkRequest[]): void {
  for (const request of requests) {
    const url = request.args.data.url;

    let summary = getOrMakeSummaryByEntity(thirdPartySummary, request, url);
    if (summary) {
      summary.transferSize += request.args.data.encodedDataLength;
    }

    summary = getOrMakeSummaryByURL(thirdPartySummary, url);
    if (summary) {
      summary.transferSize += request.args.data.encodedDataLength;
    }
  }
}

/**
 * @param networkRequests Won't be filtered by trace bounds, so callers should ensure it is filtered.
 */
export function summarizeThirdParties(
    parsedTrace: Handlers.Types.ParsedTrace, traceBounds: Types.Timing.TraceWindowMicro,
    networkRequests: Types.Events.SyntheticNetworkRequest[]): ThirdPartySummary {
  const thirdPartySummary: ThirdPartySummary = {
    byEntity: new Map(),
    byUrl: new Map(),
    urlsByEntity: new Map(),
    eventsByEntity: new Map(),
    madeUpEntityCache: new Map(),
  };

  collectMainThreadActivity(thirdPartySummary, parsedTrace, traceBounds);
  collectNetworkActivity(thirdPartySummary, networkRequests);

  return thirdPartySummary;
}

function getSummaryMapWithMapping(
    events: Types.Events.Event[], entityByEvent: Map<Types.Events.Event, Handlers.Helpers.Entity>,
    eventsByEntity: Map<Handlers.Helpers.Entity, Types.Events.Event[]>): ThirdPartySummary {
  const byEvent = new Map<Types.Events.Event, Summary>();
  const byEntity = new Map<Handlers.Helpers.Entity, Summary>();
  const defaultSummary: Summary = {transferSize: 0, mainThreadTime: Types.Timing.Micro(0)};

  for (const event of events) {
    const urlSummary = byEvent.get(event) || {...defaultSummary};
    if (Types.Events.isSyntheticNetworkRequest(event)) {
      urlSummary.transferSize += event.args.data.encodedDataLength;
    }
    byEvent.set(event, urlSummary);
  }

  // Map each request's stat to a particular entity.
  for (const [request, requestSummary] of byEvent.entries()) {
    const entity = entityByEvent.get(request);
    if (!entity) {
      byEvent.delete(request);
      continue;
    }

    const entitySummary = byEntity.get(entity) || {...defaultSummary};
    entitySummary.transferSize += requestSummary.transferSize;
    byEntity.set(entity, entitySummary);
  }

  return {byEntity, eventsByEntity, madeUpEntityCache: new Map(), byUrl: new Map(), urlsByEntity: new Map()};
}

// TODO(crbug.com/352244718): Remove or refactor to use summarizeThirdParties/collectMainThreadActivity/etc.
/**
 * Note: unlike summarizeThirdParties, this does not calculate mainThreadTime. The reason is that it is not
 * needed for its one use case, and when dragging the trace bounds it takes a long time to calculate.
 * If it is ever needed, we need to make getSelfTimeByUrl (see deleted code/blame) much faster (cache + bucket?).
 */
export function getSummariesAndEntitiesWithMapping(
    parsedTrace: Handlers.Types.ParsedTrace, traceBounds: Types.Timing.TraceWindowMicro,
    entityMapping: Handlers.Helpers.EntityMappings): {
  summaries: ThirdPartySummary,
  entityByEvent: Map<Types.Events.Event, Handlers.Helpers.Entity>,
} {
  const entityByEvent = new Map(entityMapping.entityByEvent);
  const eventsByEntity = new Map(entityMapping.eventsByEntity);

  // Consider events only in bounds.
  const entityByEventArr = Array.from(entityByEvent.entries());
  const filteredEntries = entityByEventArr.filter(([event]) => {
    return Helpers.Timing.eventIsInBounds(event, traceBounds);
  });
  const entityByEventFiltered = new Map(filteredEntries);

  // Consider events only in bounds.
  const eventsByEntityArr = Array.from(eventsByEntity.entries());
  const filtered = eventsByEntityArr.filter(([, events]) => {
    events.map(event => {
      return Helpers.Timing.eventIsInBounds(event, traceBounds);
    });
    return events.length > 0;
  });
  const eventsByEntityFiltered = new Map(filtered);

  const allEvents = Array.from(entityByEvent.keys());
  const summaries = getSummaryMapWithMapping(allEvents, entityByEventFiltered, eventsByEntityFiltered);

  return {summaries, entityByEvent: entityByEventFiltered};
}
