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

import type {HandlerName} from './types.js';

// Each thread contains events. Events indicate the thread and process IDs, which are
// used to store the event in the correct process thread entry below.
let unpairedAsyncEvents: Types.Events.PipelineReporter[] = [];

let legacyScreenshotEvents: Types.Events.LegacyScreenshot[] = [];
let modernScreenshotEvents: Types.Events.Screenshot[] = [];
let syntheticScreenshots: Types.Events.LegacySyntheticScreenshot[] = [];
let frameSequenceToTs: Record<string, Types.Timing.Micro> = {};

export function reset(): void {
  unpairedAsyncEvents = [];
  legacyScreenshotEvents = [];
  syntheticScreenshots = [];
  modernScreenshotEvents = [];
  frameSequenceToTs = {};
}

export function handleEvent(event: Types.Events.Event): void {
  if (Types.Events.isLegacyScreenshot(event)) {
    legacyScreenshotEvents.push(event);
  } else if (Types.Events.isScreenshot(event)) {
    modernScreenshotEvents.push(event);
  } else if (Types.Events.isPipelineReporter(event)) {
    unpairedAsyncEvents.push(event);
  }
}

export async function finalize(): Promise<void> {
  const pipelineReporterEvents = Helpers.Trace.createMatchedSortedSyntheticEvents(unpairedAsyncEvents);

  frameSequenceToTs = Object.fromEntries(pipelineReporterEvents.map(evt => {
    const args = evt.args.data.beginEvent.args;
    const frameReporter = 'frame_reporter' in args ? args.frame_reporter : args.chrome_frame_reporter;
    const frameSequenceId = frameReporter.frame_sequence;
    const presentationTs = Types.Timing.Micro(evt.ts + evt.dur);
    return [frameSequenceId, presentationTs];
  }));

  for (const snapshotEvent of legacyScreenshotEvents) {
    const {cat, name, ph, pid, tid} = snapshotEvent;
    const syntheticEvent = Helpers.SyntheticEvents.SyntheticEventsManager.registerSyntheticEvent<
        Types.Events.LegacySyntheticScreenshot>({
      rawSourceEvent: snapshotEvent,
      cat,
      name,
      ph,
      pid,
      tid,
      // TODO(paulirish, crbug.com/41363012): investigate why getPresentationTimestamp(snapshotEvent) seems less accurate. Resolve screenshot timing inaccuracy.
      // `getPresentationTimestamp(snapshotEvent) - snapshotEvent.ts` is how many microsec the screenshot should be adjusted to the right/later
      ts: snapshotEvent.ts,
      args: {
        dataUri: `data:image/jpg;base64,${snapshotEvent.args.snapshot}`,
      },
    });
    syntheticScreenshots.push(syntheticEvent);
  }
}

export function screenshotImageDataUri(event: Types.Events.LegacySyntheticScreenshot|Types.Events.Screenshot): string {
  if (Types.Events.isLegacySyntheticScreenshot(event)) {
    return event.args.dataUri;
  }
  return `data:image/jpg;base64,${event.args.snapshot}`;
}

/**
 * Correct the screenshot timestamps
 * The screenshot 'snapshot object' trace event has the "frame sequence number" attached as an ID.
 * We match that up with the "PipelineReporter" trace events as they terminate at presentation.
 * Presentation == when the pixels hit the screen. AKA Swap on the GPU
 */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function getPresentationTimestamp(screenshotEvent: Types.Events.LegacyScreenshot): Types.Timing.Micro {
  const frameSequence = parseInt(screenshotEvent.id, 16);
  // If it's 1, then it's an old trace (before https://crrev.com/c/4957973) and cannot be corrected.
  if (frameSequence === 1) {
    return screenshotEvent.ts;
  }
  // The screenshot trace event's `ts` reflects the "expected display time" which is ESTIMATE.
  // It is set by the compositor frame sink from the `expected_display_time`, which is based on a previously known
  // frame start PLUS the vsync interval (eg 16.6ms)
  const updatedTs = frameSequenceToTs[frameSequence];
  // Do we always find a match? No...
  // We generally don't match the very first screenshot and, sometimes, the last
  // The very first screenshot is requested immediately (even if nothing is painting). As a result there's no compositor
  // instrumentation running alongside.
  // The last one is sometimes missing as because the trace terminates right before the associated PipelineReporter is emitted.
  return updatedTs ?? screenshotEvent.ts;
}

export interface Data {
  // These are nullable because in January 2025 a CL in Chromium
  // crrev.com/c/6197645 landed which changed the format of screenshots. For a
  // given trace, it can have either "legacy" screenshot events, or "modern"
  // screenshot events, but no trace can ever contain both.
  // So, if either of these arrays are empty, we instead return `null`. This forces consumers to check the presence of the array.
  // Traces can have no screenshots if the trace category is not enabled, so it
  // is possible for a trace to return null for both of these arrays.
  legacySyntheticScreenshots: Types.Events.LegacySyntheticScreenshot[]|null;
  screenshots: Types.Events.Screenshot[]|null;
}

export function data(): Data {
  return {
    legacySyntheticScreenshots: syntheticScreenshots.length ? syntheticScreenshots : null,
    screenshots: modernScreenshotEvents.length ? modernScreenshotEvents : null,
  };
}

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