// Copyright 2017 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 type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Formatter from '../formatter/formatter.js';
import * as TextUtils from '../text_utils/text_utils.js';
import * as Workspace from '../workspace/workspace.js';

import {ContentProviderBasedProject} from './ContentProviderBasedProject.js';
import type {CSSWorkspaceBinding} from './CSSWorkspaceBinding.js';
import type {DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js';
import {NetworkProject} from './NetworkProject.js';
import {resourceMetadata} from './ResourceUtils.js';

const styleSheetRangeMap = new WeakMap<SDK.CSSStyleSheetHeader.CSSStyleSheetHeader, TextUtils.TextRange.TextRange>();
const scriptRangeMap = new WeakMap<SDK.Script.Script, TextUtils.TextRange.TextRange>();
const boundUISourceCodes = new WeakSet<Workspace.UISourceCode.UISourceCode>();

function computeScriptRange(script: SDK.Script.Script): TextUtils.TextRange.TextRange {
  return new TextUtils.TextRange.TextRange(script.lineOffset, script.columnOffset, script.endLine, script.endColumn);
}

function computeStyleSheetRange(header: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader): TextUtils.TextRange.TextRange {
  return new TextUtils.TextRange.TextRange(header.startLine, header.startColumn, header.endLine, header.endColumn);
}

export class ResourceMapping implements SDK.TargetManager.SDKModelObserver<SDK.ResourceTreeModel.ResourceTreeModel> {
  readonly workspace: Workspace.Workspace.WorkspaceImpl;
  readonly #modelToInfo = new Map<SDK.ResourceTreeModel.ResourceTreeModel, ModelInfo>();

  #debuggerWorkspaceBinding: DebuggerWorkspaceBinding|null = null;
  #cssWorkspaceBinding: CSSWorkspaceBinding|null = null;

  constructor(targetManager: SDK.TargetManager.TargetManager, workspace: Workspace.Workspace.WorkspaceImpl) {
    this.workspace = workspace;
    targetManager.observeModels(SDK.ResourceTreeModel.ResourceTreeModel, this);
  }

  get debuggerWorkspaceBinding(): DebuggerWorkspaceBinding|null {
    // TODO(crbug.com/458180550): Throw when this.#debuggerWorkspaceBinding is null and never return null.
    //                            The only reason we don't throw and return an instance unconditionally
    //                            is that unit tests often don't set-up both the *WorkspaceBindings.
    return this.#debuggerWorkspaceBinding;
  }

  /* {@link DebuggerWorkspaceBinding} and ResourceMapping form a cycle so we can't wire it up at ctor time. */
  set debuggerWorkspaceBinding(debuggerWorkspaceBinding: DebuggerWorkspaceBinding) {
    if (this.#debuggerWorkspaceBinding) {
      throw new Error('DebuggerWorkspaceBinding already set');
    }
    this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding;
  }

  get cssWorkspaceBinding(): CSSWorkspaceBinding|null {
    // TODO(crbug.com/458180550): Throw when this.#cssWorkspaceBinding is null and never return null.
    //                            The only reason we don't throw and return an instance unconditionally
    //                            is that unit tests often don't set-up both the *WorkspaceBindings.
    return this.#cssWorkspaceBinding;
  }

  /* {@link CSSWorkspaceBinding} and ResourceMapping form a cycle so we can't wire it up at ctor time. */
  set cssWorkspaceBinding(cssWorkspaceBinding: CSSWorkspaceBinding) {
    if (this.#cssWorkspaceBinding) {
      throw new Error('CSSWorkspaceBinding already set');
    }
    this.#cssWorkspaceBinding = cssWorkspaceBinding;
  }

  modelAdded(resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel): void {
    const info = new ModelInfo(this, resourceTreeModel);
    this.#modelToInfo.set(resourceTreeModel, info);
  }

  modelRemoved(resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel): void {
    const info = this.#modelToInfo.get(resourceTreeModel);
    if (info) {
      info.dispose();
      this.#modelToInfo.delete(resourceTreeModel);
    }
  }

  private infoForTarget(target: SDK.Target.Target): ModelInfo|null {
    const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
    return resourceTreeModel ? this.#modelToInfo.get(resourceTreeModel) || null : null;
  }

  uiSourceCodeForScript(script: SDK.Script.Script): Workspace.UISourceCode.UISourceCode|null {
    const info = this.infoForTarget(script.debuggerModel.target());
    if (!info) {
      return null;
    }

    const project = info.getProject();
    const uiSourceCode = project.uiSourceCodeForURL(script.sourceURL);
    return uiSourceCode;
  }

  cssLocationToUILocation(cssLocation: SDK.CSSModel.CSSLocation): Workspace.UISourceCode.UILocation|null {
    const header = cssLocation.header();
    if (!header) {
      return null;
    }
    const info = this.infoForTarget(cssLocation.cssModel().target());
    if (!info) {
      return null;
    }
    const uiSourceCode = info.getProject().uiSourceCodeForURL(cssLocation.url);
    if (!uiSourceCode) {
      return null;
    }
    const offset = styleSheetRangeMap.get(header) ?? computeStyleSheetRange(header);
    const lineNumber = cssLocation.lineNumber + offset.startLine - header.startLine;
    let columnNumber = cssLocation.columnNumber;
    if (cssLocation.lineNumber === header.startLine) {
      columnNumber += offset.startColumn - header.startColumn;
    }
    return uiSourceCode.uiLocation(lineNumber, columnNumber);
  }

  jsLocationToUILocation(jsLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null {
    const script = jsLocation.script();
    if (!script) {
      return null;
    }
    const info = this.infoForTarget(jsLocation.debuggerModel.target());
    if (!info) {
      return null;
    }
    const embedderName = script.embedderName();
    if (!embedderName) {
      return null;
    }
    const uiSourceCode = info.getProject().uiSourceCodeForURL(embedderName);
    if (!uiSourceCode) {
      return null;
    }
    const {startLine, startColumn} = scriptRangeMap.get(script) ?? computeScriptRange(script);
    let {lineNumber, columnNumber} = jsLocation;
    if (lineNumber === script.lineOffset) {
      columnNumber += startColumn - script.columnOffset;
    }
    lineNumber += startLine - script.lineOffset;
    if (script.hasSourceURL) {
      if (lineNumber === 0) {
        columnNumber += script.columnOffset;
      }
      lineNumber += script.lineOffset;
    }
    return uiSourceCode.uiLocation(lineNumber, columnNumber);
  }

  uiLocationToJSLocations(uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber: number):
      SDK.DebuggerModel.Location[] {
    if (!boundUISourceCodes.has(uiSourceCode)) {
      return [];
    }
    const target = NetworkProject.targetForUISourceCode(uiSourceCode);
    if (!target) {
      return [];
    }
    const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
    if (!debuggerModel) {
      return [];
    }
    const locations = [];
    for (const script of debuggerModel.scripts()) {
      if (script.embedderName() !== uiSourceCode.url()) {
        continue;
      }
      const range = scriptRangeMap.get(script) ?? computeScriptRange(script);
      if (!range.containsLocation(lineNumber, columnNumber)) {
        continue;
      }
      let scriptLineNumber = lineNumber;
      let scriptColumnNumber = columnNumber;
      if (script.hasSourceURL) {
        scriptLineNumber -= range.startLine;
        if (scriptLineNumber === 0) {
          scriptColumnNumber -= range.startColumn;
        }
      }
      locations.push(debuggerModel.createRawLocation(script, scriptLineNumber, scriptColumnNumber));
    }
    return locations;
  }

  uiLocationRangeToJSLocationRanges(
      uiSourceCode: Workspace.UISourceCode.UISourceCode,
      textRange: TextUtils.TextRange.TextRange): SDK.DebuggerModel.LocationRange[]|null {
    if (!boundUISourceCodes.has(uiSourceCode)) {
      return null;
    }
    const target = NetworkProject.targetForUISourceCode(uiSourceCode);
    if (!target) {
      return null;
    }
    const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
    if (!debuggerModel) {
      return null;
    }
    const ranges = [];
    for (const script of debuggerModel.scripts()) {
      if (script.embedderName() !== uiSourceCode.url()) {
        continue;
      }
      const scriptTextRange = scriptRangeMap.get(script) ?? computeScriptRange(script);
      const range = scriptTextRange.intersection(textRange);
      if (range.isEmpty()) {
        continue;
      }
      let {startLine, startColumn, endLine, endColumn} = range;
      if (script.hasSourceURL) {
        startLine -= range.startLine;
        if (startLine === 0) {
          startColumn -= range.startColumn;
        }
        endLine -= range.startLine;
        if (endLine === 0) {
          endColumn -= range.startColumn;
        }
      }
      const start = debuggerModel.createRawLocation(script, startLine, startColumn);
      const end = debuggerModel.createRawLocation(script, endLine, endColumn);
      ranges.push({start, end});
    }
    return ranges;
  }

  getMappedLines(uiSourceCode: Workspace.UISourceCode.UISourceCode): Set<number>|null {
    if (!boundUISourceCodes.has(uiSourceCode)) {
      return null;
    }
    const target = NetworkProject.targetForUISourceCode(uiSourceCode);
    if (!target) {
      return null;
    }
    const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
    if (!debuggerModel) {
      return null;
    }
    const mappedLines = new Set<number>();
    for (const script of debuggerModel.scripts()) {
      if (script.embedderName() !== uiSourceCode.url()) {
        continue;
      }
      const {startLine, endLine} = scriptRangeMap.get(script) ?? computeScriptRange(script);
      for (let line = startLine; line <= endLine; ++line) {
        mappedLines.add(line);
      }
    }
    return mappedLines;
  }

  uiLocationToCSSLocations(uiLocation: Workspace.UISourceCode.UILocation): SDK.CSSModel.CSSLocation[] {
    if (!boundUISourceCodes.has(uiLocation.uiSourceCode)) {
      return [];
    }
    const target = NetworkProject.targetForUISourceCode(uiLocation.uiSourceCode);
    if (!target) {
      return [];
    }
    const cssModel = target.model(SDK.CSSModel.CSSModel);
    if (!cssModel) {
      return [];
    }
    return cssModel.createRawLocationsByURL(
        uiLocation.uiSourceCode.url(), uiLocation.lineNumber, uiLocation.columnNumber);
  }

  async functionBoundsAtRawLocation(rawLocation: SDK.DebuggerModel.Location):
      Promise<Workspace.UISourceCode.UIFunctionBounds|null> {
    const script = rawLocation.script();
    if (!script) {
      return null;
    }
    const info = this.infoForTarget(script.debuggerModel.target());
    if (!info) {
      return null;
    }
    const embedderName = script.embedderName();
    if (!embedderName) {
      return null;
    }
    const uiSourceCode = info.getProject().uiSourceCodeForURL(embedderName);
    if (!uiSourceCode) {
      return null;
    }

    let {lineNumber, columnNumber} = rawLocation;
    lineNumber -= script.lineOffset;
    if (lineNumber === 0) {
      columnNumber -= script.columnOffset;
    }

    const scopeTreeAndText = script ? await SDK.ScopeTreeCache.scopeTreeForScript(script) : null;
    if (!scopeTreeAndText) {
      return null;
    }

    // Find the inner-most scope that maps to the given position.

    const offset = scopeTreeAndText.text.offsetFromPosition(lineNumber, columnNumber);

    const results = [];
    (function walk(nodes: Formatter.FormatterWorkerPool.ScopeTreeNode[]) {
      for (const node of nodes) {
        if (!(offset >= node.start && offset < node.end)) {
          continue;
        }
        results.push(node);
        walk(node.children);
      }
    })([scopeTreeAndText.scopeTree]);

    const result = results.findLast(
        node => node.kind === Formatter.FormatterWorkerPool.ScopeKind.FUNCTION ||
            node.kind === Formatter.FormatterWorkerPool.ScopeKind.ARROW_FUNCTION);
    if (!result) {
      return null;
    }

    // Map back to positions.
    const startPosition = scopeTreeAndText.text.positionFromOffset(result.start);
    const endPosition = scopeTreeAndText.text.positionFromOffset(result.end);

    startPosition.lineNumber += script.lineOffset;
    if (startPosition.lineNumber === script.lineOffset) {
      startPosition.columnNumber += script.columnOffset;
    }

    endPosition.lineNumber += script.lineOffset;
    if (endPosition.lineNumber === script.lineOffset) {
      endPosition.columnNumber += script.columnOffset;
    }

    const name = '';  // TODO(crbug.com/452333154): update ScopeVariableAnalysis to include function name.
    const range = new TextUtils.TextRange.TextRange(
        startPosition.lineNumber, startPosition.columnNumber, endPosition.lineNumber, endPosition.columnNumber);
    return new Workspace.UISourceCode.UIFunctionBounds(uiSourceCode, range, name);
  }

  resetForTest(target: SDK.Target.Target): void {
    const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
    const info = resourceTreeModel ? this.#modelToInfo.get(resourceTreeModel) : null;
    if (info) {
      info.resetForTest();
    }
  }
}

class ModelInfo {
  project: ContentProviderBasedProject;
  readonly #bindings = new Map<string, Binding>();
  readonly #cssModel: SDK.CSSModel.CSSModel;
  readonly #eventListeners: Common.EventTarget.EventDescriptor[];
  readonly resourceMapping: ResourceMapping;

  constructor(resourceMapping: ResourceMapping, resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel) {
    const target = resourceTreeModel.target();
    this.resourceMapping = resourceMapping;
    this.project = new ContentProviderBasedProject(
        resourceMapping.workspace, 'resources:' + target.id(), Workspace.Workspace.projectTypes.Network, '',
        false /* isServiceProject */);
    NetworkProject.setTargetForProject(this.project, target);

    const cssModel = target.model(SDK.CSSModel.CSSModel);
    console.assert(Boolean(cssModel));
    this.#cssModel = (cssModel as SDK.CSSModel.CSSModel);
    for (const frame of resourceTreeModel.frames()) {
      for (const resource of frame.getResourcesMap().values()) {
        this.addResource(resource);
      }
    }
    this.#eventListeners = [
      resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.ResourceAdded, this.resourceAdded, this),
      resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.FrameWillNavigate, this.frameWillNavigate, this),
      resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.FrameDetached, this.frameDetached, this),
      this.#cssModel.addEventListener(
          SDK.CSSModel.Events.StyleSheetChanged,
          event => {
            void this.styleSheetChanged(event);
          },
          this),
    ];
  }

  private async styleSheetChanged(event: Common.EventTarget.EventTargetEvent<SDK.CSSModel.StyleSheetChangedEvent>):
      Promise<void> {
    const header = this.#cssModel.styleSheetHeaderForId(event.data.styleSheetId);
    if (!header || !header.isInline || (header.isInline && header.isMutable)) {
      return;
    }
    const binding = this.#bindings.get(header.resourceURL());
    if (!binding) {
      return;
    }
    await binding.styleSheetChanged(header, event.data.edit || null);
  }

  private acceptsResource(resource: SDK.Resource.Resource): boolean {
    const resourceType = resource.resourceType();
    // Only load selected resource types from resources.
    if (resourceType !== Common.ResourceType.resourceTypes.Image &&
        resourceType !== Common.ResourceType.resourceTypes.Font &&
        resourceType !== Common.ResourceType.resourceTypes.Document &&
        resourceType !== Common.ResourceType.resourceTypes.Manifest &&
        resourceType !== Common.ResourceType.resourceTypes.Fetch &&
        resourceType !== Common.ResourceType.resourceTypes.XHR) {
      return false;
    }

    // Ignore non-images and non-fonts.
    if (resourceType === Common.ResourceType.resourceTypes.Image && resource.mimeType &&
        !resource.mimeType.startsWith('image')) {
      return false;
    }
    if (resourceType === Common.ResourceType.resourceTypes.Font && resource.mimeType &&
        !resource.mimeType.includes('font')) {
      return false;
    }
    if ((resourceType === Common.ResourceType.resourceTypes.Image ||
         resourceType === Common.ResourceType.resourceTypes.Font) &&
        Common.ParsedURL.schemeIs(resource.contentURL(), 'data:')) {
      return false;
    }
    return true;
  }

  private resourceAdded(event: Common.EventTarget.EventTargetEvent<SDK.Resource.Resource>): void {
    this.addResource(event.data);
  }

  private addResource(resource: SDK.Resource.Resource): void {
    if (!this.acceptsResource(resource)) {
      return;
    }

    let binding = this.#bindings.get(resource.url);
    if (!binding) {
      binding = new Binding(this, resource);
      this.#bindings.set(resource.url, binding);
    } else {
      binding.addResource(resource);
    }
  }

  private removeFrameResources(frame: SDK.ResourceTreeModel.ResourceTreeFrame): void {
    for (const resource of frame.resources()) {
      if (!this.acceptsResource(resource)) {
        continue;
      }
      const binding = this.#bindings.get(resource.url);
      if (!binding) {
        continue;
      }
      if (binding.resources.size === 1) {
        binding.dispose();
        this.#bindings.delete(resource.url);
      } else {
        binding.removeResource(resource);
      }
    }
  }

  private frameWillNavigate(event: Common.EventTarget.EventTargetEvent<SDK.ResourceTreeModel.ResourceTreeFrame>): void {
    this.removeFrameResources(event.data);
  }

  private frameDetached(
      event: Common.EventTarget.EventTargetEvent<{frame: SDK.ResourceTreeModel.ResourceTreeFrame, isSwap: boolean}>):
      void {
    this.removeFrameResources(event.data.frame);
  }

  resetForTest(): void {
    for (const binding of this.#bindings.values()) {
      binding.dispose();
    }
    this.#bindings.clear();
  }

  dispose(): void {
    Common.EventTarget.removeEventListeners(this.#eventListeners);
    for (const binding of this.#bindings.values()) {
      binding.dispose();
    }
    this.#bindings.clear();
    this.project.removeProject();
  }

  getProject(): ContentProviderBasedProject {
    return this.project;
  }
}

class Binding implements TextUtils.ContentProvider.ContentProvider {
  readonly resources: Set<SDK.Resource.Resource>;
  readonly #project: ContentProviderBasedProject;
  readonly #uiSourceCode: Workspace.UISourceCode.UISourceCode;
  #edits: Array<{
    stylesheet: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader,
    edit: SDK.CSSModel.Edit|null,
  }> = [];

  readonly #debuggerWorkspaceBinding: DebuggerWorkspaceBinding|null;
  readonly #cssWorkspaceBinding: CSSWorkspaceBinding|null;

  constructor(modelInfo: ModelInfo, resource: SDK.Resource.Resource) {
    this.resources = new Set([resource]);
    this.#project = modelInfo.project;
    this.#debuggerWorkspaceBinding = modelInfo.resourceMapping.debuggerWorkspaceBinding;
    this.#cssWorkspaceBinding = modelInfo.resourceMapping.cssWorkspaceBinding;

    this.#uiSourceCode = this.#project.createUISourceCode(resource.url, resource.contentType());
    boundUISourceCodes.add(this.#uiSourceCode);
    if (resource.frameId) {
      NetworkProject.setInitialFrameAttribution(this.#uiSourceCode, resource.frameId);
    }
    this.#project.addUISourceCodeWithProvider(this.#uiSourceCode, this, resourceMetadata(resource), resource.mimeType);

    void Promise.all([
      ...this.inlineScripts().map(script => this.#debuggerWorkspaceBinding?.updateLocations(script)),
      ...this.inlineStyles().map(style => this.#cssWorkspaceBinding?.updateLocations(style)),
    ]);
  }

  private inlineStyles(): SDK.CSSStyleSheetHeader.CSSStyleSheetHeader[] {
    const target = NetworkProject.targetForUISourceCode(this.#uiSourceCode);
    const stylesheets: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader[] = [];
    if (!target) {
      return stylesheets;
    }
    const cssModel = target.model(SDK.CSSModel.CSSModel);
    if (cssModel) {
      for (const headerId of cssModel.getStyleSheetIdsForURL(this.#uiSourceCode.url())) {
        const header = cssModel.styleSheetHeaderForId(headerId);
        if (header) {
          stylesheets.push(header);
        }
      }
    }
    return stylesheets;
  }

  private inlineScripts(): SDK.Script.Script[] {
    const target = NetworkProject.targetForUISourceCode(this.#uiSourceCode);
    if (!target) {
      return [];
    }
    const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
    if (!debuggerModel) {
      return [];
    }
    return debuggerModel.scripts().filter(script => script.embedderName() === this.#uiSourceCode.url());
  }

  async styleSheetChanged(stylesheet: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader, edit: SDK.CSSModel.Edit|null):
      Promise<void> {
    this.#edits.push({stylesheet, edit});
    if (this.#edits.length > 1) {
      return;
    }  // There is already a styleSheetChanged loop running

    const content = await this.#uiSourceCode.requestContentData();
    if (!TextUtils.ContentData.ContentData.isError(content)) {
      await this.innerStyleSheetChanged(content.text);
    }
    this.#edits = [];
  }

  private async innerStyleSheetChanged(content: string): Promise<void> {
    const scripts = this.inlineScripts();
    const styles = this.inlineStyles();
    let text: TextUtils.Text.Text = new TextUtils.Text.Text(content);
    for (const data of this.#edits) {
      const edit = data.edit;
      if (!edit) {
        continue;
      }
      const stylesheet = data.stylesheet;
      const startLocation = styleSheetRangeMap.get(stylesheet) ?? computeStyleSheetRange(stylesheet);

      const oldRange = edit.oldRange.relativeFrom(startLocation.startLine, startLocation.startColumn);
      const newRange = edit.newRange.relativeFrom(startLocation.startLine, startLocation.startColumn);
      text = new TextUtils.Text.Text(text.replaceRange(oldRange, edit.newText));
      const updatePromises = [];
      for (const script of scripts) {
        const range = scriptRangeMap.get(script) ?? computeScriptRange(script);
        if (!range.follows(oldRange)) {
          continue;
        }
        scriptRangeMap.set(script, range.rebaseAfterTextEdit(oldRange, newRange));
        updatePromises.push(this.#debuggerWorkspaceBinding?.updateLocations(script));
      }
      for (const style of styles) {
        const range = styleSheetRangeMap.get(style) ?? computeStyleSheetRange(style);
        if (!range.follows(oldRange)) {
          continue;
        }
        styleSheetRangeMap.set(style, range.rebaseAfterTextEdit(oldRange, newRange));
        updatePromises.push(this.#cssWorkspaceBinding?.updateLocations(style));
      }
      await Promise.all(updatePromises);
    }
    this.#uiSourceCode.addRevision(text.value());
  }

  addResource(resource: SDK.Resource.Resource): void {
    this.resources.add(resource);
    if (resource.frameId) {
      NetworkProject.addFrameAttribution(this.#uiSourceCode, resource.frameId);
    }
  }

  removeResource(resource: SDK.Resource.Resource): void {
    this.resources.delete(resource);
    if (resource.frameId) {
      NetworkProject.removeFrameAttribution(this.#uiSourceCode, resource.frameId);
    }
  }

  dispose(): void {
    this.#project.removeUISourceCode(this.#uiSourceCode.url());
    void Promise.all([
      ...this.inlineScripts().map(script => this.#debuggerWorkspaceBinding?.updateLocations(script)),
      ...this.inlineStyles().map(style => this.#cssWorkspaceBinding?.updateLocations(style)),
    ]);
  }

  private firstResource(): SDK.Resource.Resource {
    console.assert(this.resources.size > 0);
    return this.resources.values().next().value as SDK.Resource.Resource;
  }

  contentURL(): Platform.DevToolsPath.UrlString {
    return this.firstResource().contentURL();
  }

  contentType(): Common.ResourceType.ResourceType {
    return this.firstResource().contentType();
  }

  requestContentData(): Promise<TextUtils.ContentData.ContentDataOrError> {
    return this.firstResource().requestContentData();
  }

  searchInContent(query: string, caseSensitive: boolean, isRegex: boolean):
      Promise<TextUtils.ContentProvider.SearchMatch[]> {
    return this.firstResource().searchInContent(query, caseSensitive, isRegex);
  }
}
