// Copyright 2014 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 Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import type * as StackTrace from '../stack_trace/stack_trace.js';
// eslint-disable-next-line @devtools/es-modules-import
import * as StackTraceImpl from '../stack_trace/stack_trace_impl.js';
import type * as TextUtils from '../text_utils/text_utils.js';
import * as Workspace from '../workspace/workspace.js';

import {CompilerScriptMapping} from './CompilerScriptMapping.js';
import {DebuggerLanguagePluginManager} from './DebuggerLanguagePlugins.js';
import {DefaultScriptMapping} from './DefaultScriptMapping.js';
import {type LiveLocation, type LiveLocationPool, LiveLocationWithPool} from './LiveLocation.js';
import {NetworkProject} from './NetworkProject.js';
import type {ResourceMapping} from './ResourceMapping.js';
import {type ResourceScriptFile, ResourceScriptMapping} from './ResourceScriptMapping.js';

export class DebuggerWorkspaceBinding implements SDK.TargetManager.SDKModelObserver<SDK.DebuggerModel.DebuggerModel> {
  readonly resourceMapping: ResourceMapping;
  readonly #debuggerModelToData: Map<SDK.DebuggerModel.DebuggerModel, ModelData>;
  readonly #liveLocationPromises: Set<unknown>;
  readonly pluginManager: DebuggerLanguagePluginManager;
  readonly ignoreListManager: Workspace.IgnoreListManager.IgnoreListManager;
  readonly workspace: Workspace.Workspace.WorkspaceImpl;

  constructor(
      resourceMapping: ResourceMapping, targetManager: SDK.TargetManager.TargetManager,
      ignoreListManager: Workspace.IgnoreListManager.IgnoreListManager, workspace: Workspace.Workspace.WorkspaceImpl) {
    this.resourceMapping = resourceMapping;
    this.resourceMapping.debuggerWorkspaceBinding = this;
    this.ignoreListManager = ignoreListManager;
    this.workspace = workspace;

    this.#debuggerModelToData = new Map();
    targetManager.observeModels(SDK.DebuggerModel.DebuggerModel, this);
    this.ignoreListManager.addEventListener(
        Workspace.IgnoreListManager.Events.IGNORED_SCRIPT_RANGES_UPDATED, event => this.updateLocations(event.data));

    this.#liveLocationPromises = new Set();

    this.pluginManager = new DebuggerLanguagePluginManager(targetManager, resourceMapping.workspace, this);
  }

  setFunctionRanges(
      uiSourceCode: Workspace.UISourceCode.UISourceCode,
      ranges: SDK.SourceMapFunctionRanges.NamedFunctionRange[]): void {
    for (const modelData of this.#debuggerModelToData.values()) {
      modelData.compilerMapping.setFunctionRanges(uiSourceCode, ranges);
    }
  }

  static instance(opts: {
    forceNew: boolean|null,
    resourceMapping: ResourceMapping|null,
    targetManager: SDK.TargetManager.TargetManager|null,
    ignoreListManager: Workspace.IgnoreListManager.IgnoreListManager|null,
    workspace: Workspace.Workspace.WorkspaceImpl|null,
  } = {forceNew: null, resourceMapping: null, targetManager: null, ignoreListManager: null, workspace: null}):
      DebuggerWorkspaceBinding {
    const {forceNew, resourceMapping, targetManager, ignoreListManager, workspace} = opts;
    if (forceNew) {
      if (!resourceMapping || !targetManager || !ignoreListManager || !workspace) {
        throw new Error(
            `Unable to create DebuggerWorkspaceBinding: resourceMapping, targetManager and IgnoreLIstManager must be provided: ${
                new Error().stack}`);
      }

      Root.DevToolsContext.globalInstance().set(
          DebuggerWorkspaceBinding,
          new DebuggerWorkspaceBinding(resourceMapping, targetManager, ignoreListManager, workspace));
    }

    return Root.DevToolsContext.globalInstance().get(DebuggerWorkspaceBinding);
  }

  static removeInstance(): void {
    Root.DevToolsContext.globalInstance().delete(DebuggerWorkspaceBinding);
  }

  private async computeAutoStepRanges(mode: SDK.DebuggerModel.StepMode, callFrame: SDK.DebuggerModel.CallFrame):
      Promise<SDK.DebuggerModel.LocationRange[]> {
    function contained(location: SDK.DebuggerModel.Location, range: SDK.DebuggerModel.LocationRange): boolean {
      const {start, end} = range;
      if (start.scriptId !== location.scriptId) {
        return false;
      }
      if (location.lineNumber < start.lineNumber || location.lineNumber > end.lineNumber) {
        return false;
      }
      if (location.lineNumber === start.lineNumber && location.columnNumber < start.columnNumber) {
        return false;
      }
      if (location.lineNumber === end.lineNumber && location.columnNumber >= end.columnNumber) {
        return false;
      }
      return true;
    }

    const rawLocation = callFrame.location();
    if (!rawLocation) {
      return [];
    }
    const pluginManager = this.pluginManager;
    let ranges: SDK.DebuggerModel.LocationRange[] = [];
    if (mode === SDK.DebuggerModel.StepMode.STEP_OUT) {
      // Step out of inline function.
      return await pluginManager.getInlinedFunctionRanges(rawLocation);
    }
    const uiLocation = await pluginManager.rawLocationToUILocation(rawLocation);
    if (uiLocation) {
      ranges = await pluginManager.uiLocationToRawLocationRanges(
                   uiLocation.uiSourceCode, uiLocation.lineNumber, uiLocation.columnNumber) ||
          [];
      // TODO(bmeurer): Remove the {rawLocation} from the {ranges}?
      ranges = ranges.filter(range => contained(rawLocation, range));
      if (mode === SDK.DebuggerModel.StepMode.STEP_OVER) {
        // Step over an inlined function.
        ranges = ranges.concat(await pluginManager.getInlinedCalleesRanges(rawLocation));
      }
      return ranges;
    }

    const compilerMapping = this.#debuggerModelToData.get(rawLocation.debuggerModel)?.compilerMapping;
    if (!compilerMapping) {
      return [];
    }
    ranges = compilerMapping.getLocationRangesForSameSourceLocation(rawLocation);
    ranges = ranges.filter(range => contained(rawLocation, range));
    return ranges;
  }

  modelAdded(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
    debuggerModel.setBeforePausedCallback(this.shouldPause.bind(this));
    this.#debuggerModelToData.set(debuggerModel, new ModelData(debuggerModel, this));
    debuggerModel.setComputeAutoStepRangesCallback(this.computeAutoStepRanges.bind(this));
  }

  modelRemoved(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
    debuggerModel.setComputeAutoStepRangesCallback(null);
    const modelData = this.#debuggerModelToData.get(debuggerModel);
    if (modelData) {
      modelData.dispose();
      this.#debuggerModelToData.delete(debuggerModel);
    }
  }

  /**
   * The promise returned by this function is resolved once all *currently*
   * pending LiveLocations are processed.
   */
  async pendingLiveLocationChangesPromise(): Promise<void> {
    await Promise.all(this.#liveLocationPromises);
  }

  private recordLiveLocationChange(promise: Promise<unknown>): void {
    void promise.then(() => {
      this.#liveLocationPromises.delete(promise);
    });
    this.#liveLocationPromises.add(promise);
  }

  async updateLocations(script: SDK.Script.Script): Promise<void> {
    const stackTraceUpdatePromise = script.target()
                                        .model(StackTraceImpl.StackTraceModel.StackTraceModel)
                                        ?.scriptInfoChanged(script, this.#translateRawFrames.bind(this));
    if (stackTraceUpdatePromise) {
      this.recordLiveLocationChange(stackTraceUpdatePromise);
    }

    const updatePromises = [stackTraceUpdatePromise];
    const modelData = this.#debuggerModelToData.get(script.debuggerModel);
    if (modelData) {
      const updatePromise = modelData.updateLocations(script);
      this.recordLiveLocationChange(updatePromise);
      updatePromises.push(updatePromise);
    }

    await Promise.all(updatePromises);
  }

  async createStackTraceFromProtocolRuntime(stackTrace: Protocol.Runtime.StackTrace, target: SDK.Target.Target):
      Promise<StackTrace.StackTrace.StackTrace> {
    const model =
        target.model(StackTraceImpl.StackTraceModel.StackTraceModel) as StackTraceImpl.StackTraceModel.StackTraceModel;
    const stackTracePromise = model.createFromProtocolRuntime(stackTrace, this.#translateRawFrames.bind(this));
    this.recordLiveLocationChange(stackTracePromise);
    return await stackTracePromise;
  }

  async createStackTraceFromDebuggerPaused(
      pausedDetails: SDK.DebuggerModel.DebuggerPausedDetails,
      target: SDK.Target.Target): Promise<StackTrace.StackTrace.DebuggableStackTrace> {
    const model =
        target.model(StackTraceImpl.StackTraceModel.StackTraceModel) as StackTraceImpl.StackTraceModel.StackTraceModel;
    const stackTracePromise = model.createFromDebuggerPaused(pausedDetails, this.#translateRawFrames.bind(this));
    this.recordLiveLocationChange(stackTracePromise);
    return await stackTracePromise;
  }

  async createLiveLocation(
      rawLocation: SDK.DebuggerModel.Location, updateDelegate: (arg0: LiveLocation) => Promise<void>,
      locationPool: LiveLocationPool): Promise<Location|null> {
    const modelData = this.#debuggerModelToData.get(rawLocation.debuggerModel);
    if (!modelData) {
      return null;
    }
    const liveLocationPromise = modelData.createLiveLocation(rawLocation, updateDelegate, locationPool);
    this.recordLiveLocationChange(liveLocationPromise);
    return await liveLocationPromise;
  }

  async createStackTraceTopFrameLiveLocation(
      rawLocations: SDK.DebuggerModel.Location[], updateDelegate: (arg0: LiveLocation) => Promise<void>,
      locationPool: LiveLocationPool): Promise<LiveLocation> {
    console.assert(rawLocations.length > 0);
    const locationPromise =
        StackTraceTopFrameLocation.createStackTraceTopFrameLocation(rawLocations, this, updateDelegate, locationPool);
    this.recordLiveLocationChange(locationPromise);
    return await locationPromise;
  }

  async rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location):
      Promise<Workspace.UISourceCode.UILocation|null> {
    const uiLocation = await this.pluginManager.rawLocationToUILocation(rawLocation);
    if (uiLocation) {
      return uiLocation;
    }
    const modelData = this.#debuggerModelToData.get(rawLocation.debuggerModel);
    return modelData ? modelData.rawLocationToUILocation(rawLocation) : null;
  }

  uiSourceCodeForSourceMapSourceURL(
      debuggerModel: SDK.DebuggerModel.DebuggerModel, url: Platform.DevToolsPath.UrlString,
      isContentScript: boolean): Workspace.UISourceCode.UISourceCode|null {
    const modelData = this.#debuggerModelToData.get(debuggerModel);
    if (!modelData) {
      return null;
    }
    return modelData.compilerMapping.uiSourceCodeForURL(url, isContentScript);
  }

  async uiSourceCodeForSourceMapSourceURLPromise(
      debuggerModel: SDK.DebuggerModel.DebuggerModel, url: Platform.DevToolsPath.UrlString,
      isContentScript: boolean): Promise<Workspace.UISourceCode.UISourceCode> {
    const uiSourceCode = this.uiSourceCodeForSourceMapSourceURL(debuggerModel, url, isContentScript);
    return await (uiSourceCode || this.waitForUISourceCodeAdded(url, debuggerModel.target()));
  }

  async uiSourceCodeForDebuggerLanguagePluginSourceURLPromise(
      debuggerModel: SDK.DebuggerModel.DebuggerModel,
      url: Platform.DevToolsPath.UrlString): Promise<Workspace.UISourceCode.UISourceCode|null> {
    const uiSourceCode = this.pluginManager.uiSourceCodeForURL(debuggerModel, url);
    return await (uiSourceCode || this.waitForUISourceCodeAdded(url, debuggerModel.target()));
  }

  uiSourceCodeForScript(script: SDK.Script.Script): Workspace.UISourceCode.UISourceCode|null {
    const modelData = this.#debuggerModelToData.get(script.debuggerModel);
    if (!modelData) {
      return null;
    }
    return modelData.uiSourceCodeForScript(script);
  }

  waitForUISourceCodeAdded(url: Platform.DevToolsPath.UrlString, target: SDK.Target.Target):
      Promise<Workspace.UISourceCode.UISourceCode> {
    return new Promise(resolve => {
      const descriptor = this.workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, event => {
        const uiSourceCode = event.data;
        if (uiSourceCode.url() === url && NetworkProject.targetForUISourceCode(uiSourceCode) === target) {
          this.workspace.removeEventListener(Workspace.Workspace.Events.UISourceCodeAdded, descriptor.listener);
          resolve(uiSourceCode);
        }
      });
    });
  }

  async uiLocationToRawLocations(
      uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number,
      columnNumber?: number): Promise<SDK.DebuggerModel.Location[]> {
    const locations = await this.pluginManager.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber);
    if (locations) {
      return locations;
    }
    for (const modelData of this.#debuggerModelToData.values()) {
      const locations = modelData.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber);
      if (locations.length) {
        return locations;
      }
    }
    return [];
  }

  /**
   * Computes all the raw location ranges that intersect with the {@link textRange} in the given
   * {@link uiSourceCode}. The reverse mappings of the returned ranges must not be fully contained
   * with the {@link textRange} and it's the responsibility of the caller to appropriately filter or
   * clamp if desired.
   *
   * It's important to note that for a contiguous range in the {@link uiSourceCode} there can be a
   * variety of non-contiguous raw location ranges that intersect with the {@link textRange}. A
   * simple example is that of an HTML document with multiple inline `<script>`s in the same line,
   * so just asking for the raw locations in this single line will return a set of location ranges
   * in different scripts.
   *
   * This method returns an empty array if this {@link uiSourceCode} is not provided by any of the
   * mappings for this instance.
   *
   * @param uiSourceCode the {@link UISourceCode} to which the {@link textRange} belongs.
   * @param textRange the text range in terms of the UI.
   * @returns the list of raw location ranges that intersect with the text range or `[]` if
   *          the {@link uiSourceCode} does not belong to this instance.
   */
  async uiLocationRangeToRawLocationRanges(
      uiSourceCode: Workspace.UISourceCode.UISourceCode,
      textRange: TextUtils.TextRange.TextRange): Promise<SDK.DebuggerModel.LocationRange[]> {
    const ranges = await this.pluginManager.uiLocationRangeToRawLocationRanges(uiSourceCode, textRange);
    if (ranges) {
      return ranges;
    }
    for (const modelData of this.#debuggerModelToData.values()) {
      const ranges = modelData.uiLocationRangeToRawLocationRanges(uiSourceCode, textRange);
      if (ranges) {
        return ranges;
      }
    }
    return [];
  }

  async functionBoundsAtRawLocation(rawLocation: SDK.DebuggerModel.Location):
      Promise<Workspace.UISourceCode.UIFunctionBounds|null> {
    // TODO(crbug.com/463452667): first try pluginManager.
    const modelData = this.#debuggerModelToData.get(rawLocation.debuggerModel);
    return modelData ? await modelData.functionBoundsAtRawLocation(rawLocation) : null;
  }

  async normalizeUILocation(uiLocation: Workspace.UISourceCode.UILocation): Promise<Workspace.UISourceCode.UILocation> {
    const rawLocations =
        await this.uiLocationToRawLocations(uiLocation.uiSourceCode, uiLocation.lineNumber, uiLocation.columnNumber);
    for (const location of rawLocations) {
      const uiLocationCandidate = await this.rawLocationToUILocation(location);
      if (uiLocationCandidate) {
        return uiLocationCandidate;
      }
    }
    return uiLocation;
  }

  /**
   * Computes the set of lines in the {@link uiSourceCode} that map to scripts by either looking at
   * the debug info (if any) or checking for inline scripts within documents. If this set cannot be
   * computed or all the lines in the {@link uiSourceCode} correspond to lines in a script, `null`
   * is returned here.
   *
   * @param uiSourceCode the source entity.
   * @returns a set of known mapped lines for {@link uiSourceCode} or `null` if it's impossible to
   *          determine the set or the {@link uiSourceCode} does not map to or include any scripts.
   */
  async getMappedLines(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<Set<number>|null> {
    for (const modelData of this.#debuggerModelToData.values()) {
      const mappedLines = modelData.getMappedLines(uiSourceCode);
      if (mappedLines !== null) {
        return mappedLines;
      }
    }
    return await this.pluginManager.getMappedLines(uiSourceCode);
  }

  scriptFile(uiSourceCode: Workspace.UISourceCode.UISourceCode, debuggerModel: SDK.DebuggerModel.DebuggerModel):
      ResourceScriptFile|null {
    const modelData = this.#debuggerModelToData.get(debuggerModel);
    return modelData ? modelData.getResourceScriptMapping().scriptFile(uiSourceCode) : null;
  }

  scriptsForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): SDK.Script.Script[] {
    const scripts = new Set<SDK.Script.Script>();
    this.pluginManager.scriptsForUISourceCode(uiSourceCode).forEach(script => scripts.add(script));
    for (const modelData of this.#debuggerModelToData.values()) {
      const resourceScriptFile = modelData.getResourceScriptMapping().scriptFile(uiSourceCode);
      if (resourceScriptFile?.script) {
        scripts.add(resourceScriptFile.script);
      }
      modelData.compilerMapping.scriptsForUISourceCode(uiSourceCode).forEach(script => scripts.add(script));
    }
    return [...scripts];
  }

  supportsConditionalBreakpoints(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean {
    const scripts = this.pluginManager.scriptsForUISourceCode(uiSourceCode);
    return scripts.every(script => script.isJavaScript());
  }

  resetForTest(target: SDK.Target.Target): void {
    const debuggerModel = (target.model(SDK.DebuggerModel.DebuggerModel) as SDK.DebuggerModel.DebuggerModel);
    const modelData = this.#debuggerModelToData.get(debuggerModel);
    if (modelData) {
      modelData.getResourceScriptMapping().resetForTest();
    }
  }

  removeLiveLocation(location: Location): void {
    const modelData = this.#debuggerModelToData.get(location.rawLocation.debuggerModel);
    if (modelData) {
      modelData.disposeLocation(location);
    }
  }

  private async shouldPause(
      debuggerPausedDetails: SDK.DebuggerModel.DebuggerPausedDetails,
      autoSteppingContext: SDK.DebuggerModel.Location|null): Promise<boolean> {
    // This function returns false if the debugger should continue stepping
    const {callFrames: [frame]} = debuggerPausedDetails;
    if (!frame) {
      return false;
    }
    const functionLocation = frame.functionLocation();
    if (!autoSteppingContext || debuggerPausedDetails.reason !== Protocol.Debugger.PausedEventReason.Step ||
        !functionLocation || !frame.script.isWasm() || !Common.Settings.moduleSetting('wasm-auto-stepping').get() ||
        !this.pluginManager.hasPluginForScript(frame.script)) {
      return true;
    }
    const uiLocation = await this.pluginManager.rawLocationToUILocation(frame.location());
    if (uiLocation) {
      return true;
    }

    return autoSteppingContext.script() !== functionLocation.script() ||
        autoSteppingContext.columnNumber !== functionLocation.columnNumber ||
        autoSteppingContext.lineNumber !== functionLocation.lineNumber;
  }

  async #translateRawFrames(frames: readonly StackTraceImpl.Trie.RawFrame[], target: SDK.Target.Target):
      ReturnType<StackTraceImpl.StackTraceModel.TranslateRawFrames> {
    const rawFrames = frames.slice(0);
    const translatedFrames: Awaited<ReturnType<StackTraceImpl.StackTraceModel.TranslateRawFrames>> = [];
    while (rawFrames.length) {
      await this.#translateRawFramesStep(rawFrames, translatedFrames, target);
    }
    return translatedFrames;
  }

  async #translateRawFramesStep(
      rawFrames: StackTraceImpl.Trie.RawFrame[],
      translatedFrames: Awaited<ReturnType<StackTraceImpl.StackTraceModel.TranslateRawFrames>>,
      target: SDK.Target.Target): Promise<void> {
    if (await this.pluginManager.translateRawFramesStep(rawFrames, translatedFrames, target)) {
      return;
    }

    const modelData =
        this.#debuggerModelToData.get(target.model(SDK.DebuggerModel.DebuggerModel) as SDK.DebuggerModel.DebuggerModel);
    if (modelData) {
      await modelData.translateRawFramesStep(rawFrames, translatedFrames);
      return;
    }

    const frame = rawFrames.shift() as StackTraceImpl.Trie.RawFrame;
    const {url, lineNumber, columnNumber, functionName} = frame;
    translatedFrames.push([{url, line: lineNumber, column: columnNumber, name: functionName}]);
  }
}

class ModelData {
  readonly #debuggerModel: SDK.DebuggerModel.DebuggerModel;
  readonly #debuggerWorkspaceBinding: DebuggerWorkspaceBinding;
  #defaultMapping: DefaultScriptMapping;
  readonly #resourceMapping: ResourceMapping;
  #resourceScriptMapping: ResourceScriptMapping;
  readonly compilerMapping: CompilerScriptMapping;
  readonly #locations: Platform.MapUtilities.Multimap<string, Location>;

  constructor(debuggerModel: SDK.DebuggerModel.DebuggerModel, debuggerWorkspaceBinding: DebuggerWorkspaceBinding) {
    this.#debuggerModel = debuggerModel;
    this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding;

    const {workspace} = debuggerWorkspaceBinding.resourceMapping;
    this.#defaultMapping = new DefaultScriptMapping(debuggerModel, workspace, debuggerWorkspaceBinding);
    this.#resourceMapping = debuggerWorkspaceBinding.resourceMapping;
    this.#resourceScriptMapping = new ResourceScriptMapping(debuggerModel, workspace, debuggerWorkspaceBinding);
    this.compilerMapping = new CompilerScriptMapping(debuggerModel, workspace, debuggerWorkspaceBinding);

    this.#locations = new Platform.MapUtilities.Multimap();
  }

  async createLiveLocation(
      rawLocation: SDK.DebuggerModel.Location, updateDelegate: (arg0: LiveLocation) => Promise<void>,
      locationPool: LiveLocationPool): Promise<Location> {
    console.assert(rawLocation.scriptId !== '');
    const scriptId = rawLocation.scriptId;
    const location = new Location(scriptId, rawLocation, this.#debuggerWorkspaceBinding, updateDelegate, locationPool);
    this.#locations.set(scriptId, location);
    await location.update();
    return location;
  }

  disposeLocation(location: Location): void {
    this.#locations.delete(location.scriptId, location);
  }

  async updateLocations(script: SDK.Script.Script): Promise<void> {
    const promises = [];
    for (const location of this.#locations.get(script.scriptId)) {
      promises.push(location.update());
    }
    await Promise.all(promises);
  }

  rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null {
    let uiLocation = this.compilerMapping.rawLocationToUILocation(rawLocation);
    uiLocation = uiLocation || this.#resourceScriptMapping.rawLocationToUILocation(rawLocation);
    uiLocation = uiLocation || this.#resourceMapping.jsLocationToUILocation(rawLocation);
    uiLocation = uiLocation || this.#defaultMapping.rawLocationToUILocation(rawLocation);
    return uiLocation;
  }

  uiSourceCodeForScript(script: SDK.Script.Script): Workspace.UISourceCode.UISourceCode|null {
    let uiSourceCode: Workspace.UISourceCode.UISourceCode|null = null;
    uiSourceCode = uiSourceCode || this.#resourceScriptMapping.uiSourceCodeForScript(script);
    uiSourceCode = uiSourceCode || this.#resourceMapping.uiSourceCodeForScript(script);
    uiSourceCode = uiSourceCode || this.#defaultMapping.uiSourceCodeForScript(script);
    return uiSourceCode;
  }

  uiLocationToRawLocations(
      uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number,
      columnNumber: number|undefined = 0): SDK.DebuggerModel.Location[] {
    // TODO(crbug.com/1153123): Revisit the `#columnNumber = 0` and also preserve `undefined` for source maps?
    let locations = this.compilerMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber);
    locations = locations.length ?
        locations :
        this.#resourceScriptMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber);
    locations = locations.length ?
        locations :
        this.#resourceMapping.uiLocationToJSLocations(uiSourceCode, lineNumber, columnNumber);
    locations = locations.length ?
        locations :
        this.#defaultMapping.uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber);
    return locations;
  }

  uiLocationRangeToRawLocationRanges(
      uiSourceCode: Workspace.UISourceCode.UISourceCode,
      textRange: TextUtils.TextRange.TextRange): SDK.DebuggerModel.LocationRange[]|null {
    let ranges = this.compilerMapping.uiLocationRangeToRawLocationRanges(uiSourceCode, textRange);
    ranges ??= this.#resourceScriptMapping.uiLocationRangeToRawLocationRanges(uiSourceCode, textRange);
    ranges ??= this.#resourceMapping.uiLocationRangeToJSLocationRanges(uiSourceCode, textRange);
    ranges ??= this.#defaultMapping.uiLocationRangeToRawLocationRanges(uiSourceCode, textRange);
    return ranges;
  }

  async functionBoundsAtRawLocation(rawLocation: SDK.DebuggerModel.Location):
      Promise<Workspace.UISourceCode.UIFunctionBounds|null> {
    let scope: Workspace.UISourceCode.UIFunctionBounds|null = null;
    // Check source maps.
    scope = scope || await this.compilerMapping.functionBoundsAtRawLocation(rawLocation);
    // Check debugger scripts.
    scope = scope || await this.#resourceScriptMapping.functionBoundsAtRawLocation(rawLocation);
    // Check inline scripts inside HTML resources.
    scope = scope || await this.#resourceMapping.functionBoundsAtRawLocation(rawLocation);
    return scope;
  }

  async translateRawFramesStep(
      rawFrames: StackTraceImpl.Trie.RawFrame[],
      translatedFrames: Awaited<ReturnType<StackTraceImpl.StackTraceModel.TranslateRawFrames>>): Promise<void> {
    if (!await this.compilerMapping.translateRawFramesStep(rawFrames, translatedFrames)) {
      this.#defaultTranslateRawFramesStep(rawFrames, translatedFrames);
    }
  }

  /** The default implementation translates one frame at a time and only translates the location, but not the function name. */
  #defaultTranslateRawFramesStep(
      rawFrames: StackTraceImpl.Trie.RawFrame[],
      translatedFrames: Awaited<ReturnType<StackTraceImpl.StackTraceModel.TranslateRawFrames>>): void {
    const frame = rawFrames.shift() as StackTraceImpl.Trie.RawFrame;
    const {scriptId, url, lineNumber, columnNumber, functionName} = frame;
    const rawLocation = scriptId ? this.#debuggerModel.createRawLocationByScriptId(scriptId, lineNumber, columnNumber) :
        url                      ? this.#debuggerModel.createRawLocationByURL(url, lineNumber, columnNumber) :
                                   null;
    if (rawLocation) {
      const uiLocation = this.rawLocationToUILocation(rawLocation);
      if (uiLocation) {
        translatedFrames.push([{
          uiSourceCode: uiLocation.uiSourceCode,
          name: functionName,
          line: uiLocation.lineNumber,
          column: uiLocation.columnNumber ?? -1
        }]);
        return;
      }
    }

    translatedFrames.push([{url, line: lineNumber, column: columnNumber, name: functionName}]);
  }

  getMappedLines(uiSourceCode: Workspace.UISourceCode.UISourceCode): Set<number>|null {
    const mappedLines = this.compilerMapping.getMappedLines(uiSourceCode);
    // TODO(crbug.com/1411431): The scripts from the ResourceMapping appear over time,
    // and there's currently no way to inform the UI to update.
    // mappedLines = mappedLines ?? this.#resourceMapping.getMappedLines(uiSourceCode);
    return mappedLines;
  }

  dispose(): void {
    this.#debuggerModel.setBeforePausedCallback(null);
    this.compilerMapping.dispose();
    this.#resourceScriptMapping.dispose();
    this.#defaultMapping.dispose();
  }

  getResourceScriptMapping(): ResourceScriptMapping {
    return this.#resourceScriptMapping;
  }
}

export class Location extends LiveLocationWithPool {
  readonly scriptId: string;
  readonly rawLocation: SDK.DebuggerModel.Location;
  readonly #binding: DebuggerWorkspaceBinding;

  constructor(
      scriptId: string, rawLocation: SDK.DebuggerModel.Location, binding: DebuggerWorkspaceBinding,
      updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool) {
    super(updateDelegate, locationPool);
    this.scriptId = scriptId;
    this.rawLocation = rawLocation;
    this.#binding = binding;
  }

  override async uiLocation(): Promise<Workspace.UISourceCode.UILocation|null> {
    const debuggerModelLocation = this.rawLocation;
    return await this.#binding.rawLocationToUILocation(debuggerModelLocation);
  }

  override dispose(): void {
    super.dispose();
    this.#binding.removeLiveLocation(this);
  }
}

class StackTraceTopFrameLocation extends LiveLocationWithPool {
  #updateScheduled: boolean;
  #current: LiveLocation|null;
  #locations: LiveLocation[]|null;
  constructor(updateDelegate: (arg0: LiveLocation) => Promise<void>, locationPool: LiveLocationPool) {
    super(updateDelegate, locationPool);
    this.#updateScheduled = true;
    this.#current = null;
    this.#locations = null;
  }

  static async createStackTraceTopFrameLocation(
      rawLocations: SDK.DebuggerModel.Location[], binding: DebuggerWorkspaceBinding,
      updateDelegate: (arg0: LiveLocation) => Promise<void>,
      locationPool: LiveLocationPool): Promise<StackTraceTopFrameLocation> {
    const location = new StackTraceTopFrameLocation(updateDelegate, locationPool);
    const locationsPromises = rawLocations.map(
        rawLocation => binding.createLiveLocation(rawLocation, location.scheduleUpdate.bind(location), locationPool));
    location.#locations = ((await Promise.all(locationsPromises)).filter(l => !!l));
    await location.updateLocation();
    return location;
  }

  override async uiLocation(): Promise<Workspace.UISourceCode.UILocation|null> {
    return this.#current ? await this.#current.uiLocation() : null;
  }

  override dispose(): void {
    super.dispose();
    if (this.#locations) {
      for (const location of this.#locations) {
        location.dispose();
      }
    }
    this.#locations = null;
    this.#current = null;
  }

  private async scheduleUpdate(): Promise<void> {
    if (this.#updateScheduled) {
      return;
    }
    this.#updateScheduled = true;
    queueMicrotask(() => {
      void this.updateLocation();
    });
  }

  private async updateLocation(): Promise<void> {
    this.#updateScheduled = false;
    if (!this.#locations || this.#locations.length === 0) {
      return;
    }

    this.#current = this.#locations[0];
    for (const location of this.#locations) {
      const uiLocation = await location.uiLocation();
      if (!uiLocation?.isIgnoreListed()) {
        this.#current = location;
        break;
      }
    }
    void this.update();
  }
}

export interface DebuggerSourceMapping {
  rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null;

  uiLocationToRawLocations(
      uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number,
      columnNumber?: number): SDK.DebuggerModel.Location[];

  uiLocationRangeToRawLocationRanges(
      uiSourceCode: Workspace.UISourceCode.UISourceCode,
      textRange: TextUtils.TextRange.TextRange): SDK.DebuggerModel.LocationRange[]|null;
}
