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

import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js';

import {CategorizedBreakpoint, Category} from './CategorizedBreakpoint.js';
import type {EventListenerPausedDetailsAuxData} from './DebuggerModel.js';
import {SDKModel} from './SDKModel.js';
import {Capability, type Target} from './Target.js';
import {type SDKModelObserver, TargetManager} from './TargetManager.js';

export const enum InstrumentationNames {
  BEFORE_BIDDER_WORKLET_BIDDING_START = 'beforeBidderWorkletBiddingStart',
  BEFORE_BIDDER_WORKLET_REPORTING_START = 'beforeBidderWorkletReportingStart',
  BEFORE_SELLER_WORKLET_SCORING_START = 'beforeSellerWorkletScoringStart',
  BEFORE_SELLER_WORKLET_REPORTING_START = 'beforeSellerWorkletReportingStart',
  SET_TIMEOUT = 'setTimeout',
  CLEAR_TIMEOUT = 'clearTimeout',
  SET_TIMEOUT_CALLBACK = 'setTimeout.callback',
  SET_INTERVAL = 'setInterval',
  CLEAR_INTERVAL = 'clearInterval',
  SET_INTERVAL_CALLBACK = 'setInterval.callback',
  SCRIPT_FIRST_STATEMENT = 'scriptFirstStatement',
  SCRIPT_BLOCKED_BY_CSP = 'scriptBlockedByCSP',
  SHARED_STORAGE_WORKLET_SCRIPT_FIRST_STATEMENT = 'sharedStorageWorkletScriptFirstStatement',
  REQUEST_ANIMATION_FRAME = 'requestAnimationFrame',
  CANCEL_ANIMATION_FRAME = 'cancelAnimationFrame',
  REQUEST_ANIMATION_FRAME_CALLBACK = 'requestAnimationFrame.callback',
  WEBGL_ERROR_FIRED = 'webglErrorFired',
  WEBGL_WARNING_FIRED = 'webglWarningFired',
  ELEMENT_SET_INNER_HTML = 'Element.setInnerHTML',
  CANVAS_CONTEXT_CREATED = 'canvasContextCreated',
  GEOLOCATION_GET_CURRENT_POSITION = 'Geolocation.getCurrentPosition',
  GEOLOCATION_WATCH_POSITION = 'Geolocation.watchPosition',
  NOTIFICATION_REQUEST_PERMISSION = 'Notification.requestPermission',
  DOM_WINDOW_CLOSE = 'DOMWindow.close',
  DOCUMENT_WRITE = 'Document.write',
  AUDIO_CONTEXT_CREATED = 'audioContextCreated',
  AUDIO_CONTEXT_CLOSED = 'audioContextClosed',
  AUDIO_CONTEXT_RESUMED = 'audioContextResumed',
  AUDIO_CONTEXT_SUSPENDED = 'audioContextSuspended',
}

export class EventBreakpointsModel extends SDKModel<void> {
  readonly agent: ProtocolProxyApi.EventBreakpointsApi;

  constructor(target: Target) {
    super(target);
    this.agent = target.eventBreakpointsAgent();
  }
}

/**
 * This implementation (as opposed to similar class in DOMDebuggerModel) is for
 * instrumentation breakpoints in targets that run JS but do not have a DOM.
 **/
class EventListenerBreakpoint extends CategorizedBreakpoint {
  readonly #targetManager: TargetManager;
  constructor(category: Category, name: string, targetManager: TargetManager) {
    super(category, name);
    this.#targetManager = targetManager;
  }

  override setEnabled(enabled: boolean): void {
    if (this.enabled() === enabled) {
      return;
    }
    super.setEnabled(enabled);
    for (const model of this.#targetManager.models(EventBreakpointsModel)) {
      this.updateOnModel(model);
    }
  }

  updateOnModel(model: EventBreakpointsModel): void {
    if (this.enabled()) {
      void model.agent.invoke_setInstrumentationBreakpoint({eventName: this.name});
    } else {
      void model.agent.invoke_removeInstrumentationBreakpoint({eventName: this.name});
    }
  }

  static readonly instrumentationPrefix = 'instrumentation:';
}

let eventBreakpointManagerInstance: EventBreakpointsManager;

export class EventBreakpointsManager implements SDKModelObserver<EventBreakpointsModel> {
  readonly #eventListenerBreakpoints: EventListenerBreakpoint[] = [];
  readonly #targetManager: TargetManager;

  constructor(targetManager: TargetManager = TargetManager.instance()) {
    this.#targetManager = targetManager;
    this.createInstrumentationBreakpoints(Category.AUCTION_WORKLET, [
      InstrumentationNames.BEFORE_BIDDER_WORKLET_BIDDING_START,
      InstrumentationNames.BEFORE_BIDDER_WORKLET_REPORTING_START,
      InstrumentationNames.BEFORE_SELLER_WORKLET_SCORING_START,
      InstrumentationNames.BEFORE_SELLER_WORKLET_REPORTING_START,
    ]);
    this.createInstrumentationBreakpoints(Category.ANIMATION, [
      InstrumentationNames.REQUEST_ANIMATION_FRAME,
      InstrumentationNames.CANCEL_ANIMATION_FRAME,
      InstrumentationNames.REQUEST_ANIMATION_FRAME_CALLBACK,
    ]);
    this.createInstrumentationBreakpoints(Category.CANVAS, [
      InstrumentationNames.CANVAS_CONTEXT_CREATED,
      InstrumentationNames.WEBGL_ERROR_FIRED,
      InstrumentationNames.WEBGL_WARNING_FIRED,
    ]);
    this.createInstrumentationBreakpoints(Category.GEOLOCATION, [
      InstrumentationNames.GEOLOCATION_GET_CURRENT_POSITION,
      InstrumentationNames.GEOLOCATION_WATCH_POSITION,
    ]);
    this.createInstrumentationBreakpoints(Category.NOTIFICATION, [
      InstrumentationNames.NOTIFICATION_REQUEST_PERMISSION,
    ]);
    this.createInstrumentationBreakpoints(Category.PARSE, [
      InstrumentationNames.ELEMENT_SET_INNER_HTML,
      InstrumentationNames.DOCUMENT_WRITE,
    ]);
    this.createInstrumentationBreakpoints(Category.SCRIPT, [
      InstrumentationNames.SCRIPT_FIRST_STATEMENT,
      InstrumentationNames.SCRIPT_BLOCKED_BY_CSP,
    ]);
    this.createInstrumentationBreakpoints(Category.SHARED_STORAGE_WORKLET, [
      InstrumentationNames.SHARED_STORAGE_WORKLET_SCRIPT_FIRST_STATEMENT,
    ]);
    this.createInstrumentationBreakpoints(Category.TIMER, [
      InstrumentationNames.SET_TIMEOUT,
      InstrumentationNames.CLEAR_TIMEOUT,
      InstrumentationNames.SET_TIMEOUT_CALLBACK,
      InstrumentationNames.SET_INTERVAL,
      InstrumentationNames.CLEAR_INTERVAL,
      InstrumentationNames.SET_INTERVAL_CALLBACK,
    ]);
    this.createInstrumentationBreakpoints(Category.WINDOW, [
      InstrumentationNames.DOM_WINDOW_CLOSE,
    ]);
    this.createInstrumentationBreakpoints(Category.WEB_AUDIO, [
      InstrumentationNames.AUDIO_CONTEXT_CREATED,
      InstrumentationNames.AUDIO_CONTEXT_CLOSED,
      InstrumentationNames.AUDIO_CONTEXT_RESUMED,
      InstrumentationNames.AUDIO_CONTEXT_SUSPENDED,
    ]);

    this.#targetManager.observeModels(EventBreakpointsModel, this);
  }

  static instance(opts: {
    forceNew: boolean|null,
    targetManager?: TargetManager,
  } = {forceNew: null}): EventBreakpointsManager {
    const {forceNew, targetManager} = opts;
    if (!eventBreakpointManagerInstance || forceNew) {
      eventBreakpointManagerInstance = new EventBreakpointsManager(targetManager);
    }

    return eventBreakpointManagerInstance;
  }

  private createInstrumentationBreakpoints(category: Category, instrumentationNames: InstrumentationNames[]): void {
    for (const instrumentationName of instrumentationNames) {
      this.#eventListenerBreakpoints.push(
          new EventListenerBreakpoint(category, instrumentationName, this.#targetManager));
    }
  }

  eventListenerBreakpoints(): EventListenerBreakpoint[] {
    return this.#eventListenerBreakpoints.slice();
  }

  resolveEventListenerBreakpoint({eventName}: EventListenerPausedDetailsAuxData): EventListenerBreakpoint|null {
    if (!eventName.startsWith(EventListenerBreakpoint.instrumentationPrefix)) {
      return null;
    }

    const instrumentationName = eventName.substring(EventListenerBreakpoint.instrumentationPrefix.length);
    return this.#eventListenerBreakpoints.find(b => b.name === instrumentationName) || null;
  }

  modelAdded(eventBreakpointModel: EventBreakpointsModel): void {
    for (const breakpoint of this.#eventListenerBreakpoints) {
      if (breakpoint.enabled()) {
        breakpoint.updateOnModel(eventBreakpointModel);
      }
    }
  }

  modelRemoved(_eventBreakpointModel: EventBreakpointsModel): void {
  }
}

SDKModel.register(EventBreakpointsModel, {capabilities: Capability.EVENT_BREAKPOINTS, autostart: false});
