// Copyright (c) 2020 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.

/* eslint-disable rulesdir/no_underscored_properties */

import * as Bindings from '../bindings/bindings.js';  // eslint-disable-line no-unused-vars
import * as Common from '../common/common.js';
import * as Platform from '../platform/platform.js';
import * as SDK from '../sdk/sdk.js';
import * as TextUtils from '../text_utils/text_utils.js';

export const enum CoverageType {
  CSS = (1 << 0),
  JavaScript = (1 << 1),
  JavaScriptPerFunction = (1 << 2),
}


export const enum SuspensionState {
  Active = 'Active',
  Suspending = 'Suspending',
  Suspended = 'Suspended',
}


// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum Events {
  CoverageUpdated = 'CoverageUpdated',
  CoverageReset = 'CoverageReset',
}


const COVERAGE_POLLING_PERIOD_MS: number = 200;

interface BacklogItem<T> {
  rawCoverageData: Array<T>;
  stamp: number;
}

export class CoverageModel extends SDK.SDKModel.SDKModel {
  _cpuProfilerModel: SDK.CPUProfilerModel.CPUProfilerModel|null;
  _cssModel: SDK.CSSModel.CSSModel|null;
  _debuggerModel: SDK.DebuggerModel.DebuggerModel|null;
  _coverageByURL: Map<string, URLCoverageInfo>;
  _coverageByContentProvider: Map<TextUtils.ContentProvider.ContentProvider, CoverageInfo>;
  _coverageUpdateTimes: Set<number>;
  _suspensionState: SuspensionState;
  _pollTimer: number|null;
  _currentPollPromise: Promise<void>|null;
  _shouldResumePollingOnResume: boolean|null;
  _jsBacklog: BacklogItem<Protocol.Profiler.ScriptCoverage>[];
  _cssBacklog: BacklogItem<Protocol.CSS.RuleUsage>[];
  _performanceTraceRecording: boolean|null;

  constructor(target: SDK.SDKModel.Target) {
    super(target);
    this._cpuProfilerModel = target.model(SDK.CPUProfilerModel.CPUProfilerModel);
    this._cssModel = target.model(SDK.CSSModel.CSSModel);
    this._debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);

    this._coverageByURL = new Map();
    this._coverageByContentProvider = new Map();

    // We keep track of the update times, because the other data-structures don't change if an
    // update doesn't change the coverage. Some visualizations want to convey to the user that
    // an update was received at a certain time, but did not result in a coverage change.
    this._coverageUpdateTimes = new Set();

    this._suspensionState = SuspensionState.Active;
    this._pollTimer = null;
    this._currentPollPromise = null;
    this._shouldResumePollingOnResume = false;
    this._jsBacklog = [];
    this._cssBacklog = [];
    this._performanceTraceRecording = false;
  }

  async start(jsCoveragePerBlock: boolean): Promise<boolean> {
    if (this._suspensionState !== SuspensionState.Active) {
      throw Error('Cannot start CoverageModel while it is not active.');
    }
    const promises = [];
    if (this._cssModel) {
      // Note there's no JS coverage since JS won't ever return
      // coverage twice, even after it's restarted.
      this._clearCSS();

      this._cssModel.addEventListener(SDK.CSSModel.Events.StyleSheetAdded, this._handleStyleSheetAdded, this);
      promises.push(this._cssModel.startCoverage());
    }
    if (this._cpuProfilerModel) {
      promises.push(
          this._cpuProfilerModel.startPreciseCoverage(jsCoveragePerBlock, this.preciseCoverageDeltaUpdate.bind(this)));
    }

    await Promise.all(promises);
    return Boolean(this._cssModel || this._cpuProfilerModel);
  }

  preciseCoverageDeltaUpdate(timestamp: number, occasion: string, coverageData: Protocol.Profiler.ScriptCoverage[]):
      void {
    this._coverageUpdateTimes.add(timestamp);
    this._backlogOrProcessJSCoverage(coverageData, timestamp);
  }

  async stop(): Promise<void> {
    await this.stopPolling();
    const promises = [];
    if (this._cpuProfilerModel) {
      promises.push(this._cpuProfilerModel.stopPreciseCoverage());
    }
    if (this._cssModel) {
      promises.push(this._cssModel.stopCoverage());
      this._cssModel.removeEventListener(SDK.CSSModel.Events.StyleSheetAdded, this._handleStyleSheetAdded, this);
    }
    await Promise.all(promises);
  }

  reset(): void {
    this._coverageByURL = new Map();
    this._coverageByContentProvider = new Map();
    this._coverageUpdateTimes = new Set();
    this.dispatchEventToListeners(Events.CoverageReset);
  }

  async startPolling(): Promise<void> {
    if (this._currentPollPromise || this._suspensionState !== SuspensionState.Active) {
      return;
    }
    await this._pollLoop();
  }

  async _pollLoop(): Promise<void> {
    this._clearTimer();
    this._currentPollPromise = this._pollAndCallback();
    await this._currentPollPromise;
    if (this._suspensionState === SuspensionState.Active || this._performanceTraceRecording) {
      this._pollTimer = window.setTimeout(() => this._pollLoop(), COVERAGE_POLLING_PERIOD_MS);
    }
  }

  async stopPolling(): Promise<void> {
    this._clearTimer();
    await this._currentPollPromise;
    this._currentPollPromise = null;
    // Do one last poll to get the final data.
    await this._pollAndCallback();
  }

  async _pollAndCallback(): Promise<void> {
    if (this._suspensionState === SuspensionState.Suspended && !this._performanceTraceRecording) {
      return;
    }
    const updates = await this._takeAllCoverage();
    // This conditional should never trigger, as all intended ways to stop
    // polling are awaiting the `_currentPollPromise` before suspending.
    console.assert(
        this._suspensionState !== SuspensionState.Suspended || Boolean(this._performanceTraceRecording),
        'CoverageModel was suspended while polling.');
    if (updates.length) {
      this.dispatchEventToListeners(Events.CoverageUpdated, updates);
    }
  }

  _clearTimer(): void {
    if (this._pollTimer) {
      clearTimeout(this._pollTimer);
      this._pollTimer = null;
    }
  }

  /**
   * Stops polling as preparation for suspension. This function is idempotent
   * due because it changes the state to suspending.
   */
  async preSuspendModel(reason?: string): Promise<void> {
    if (this._suspensionState !== SuspensionState.Active) {
      return;
    }
    this._suspensionState = SuspensionState.Suspending;
    if (reason === 'performance-timeline') {
      this._performanceTraceRecording = true;
      // Keep polling to the backlog if a performance trace is recorded.
      return;
    }
    if (this._currentPollPromise) {
      await this.stopPolling();
      this._shouldResumePollingOnResume = true;
    }
  }

  async suspendModel(_reason?: string): Promise<void> {
    this._suspensionState = SuspensionState.Suspended;
  }

  async resumeModel(): Promise<void> {
  }

  /**
   * Restarts polling after suspension. Note that the function is idempotent
   * because starting polling is idempotent.
   */
  async postResumeModel(): Promise<void> {
    this._suspensionState = SuspensionState.Active;
    this._performanceTraceRecording = false;
    if (this._shouldResumePollingOnResume) {
      this._shouldResumePollingOnResume = false;
      await this.startPolling();
    }
  }

  entries(): URLCoverageInfo[] {
    return Array.from(this._coverageByURL.values());
  }

  getCoverageForUrl(url: string): URLCoverageInfo|null {
    return this._coverageByURL.get(url) || null;
  }

  usageForRange(contentProvider: TextUtils.ContentProvider.ContentProvider, startOffset: number, endOffset: number):
      boolean|undefined {
    const coverageInfo = this._coverageByContentProvider.get(contentProvider);
    return coverageInfo && coverageInfo.usageForRange(startOffset, endOffset);
  }

  _clearCSS(): void {
    for (const entry of this._coverageByContentProvider.values()) {
      if (entry.type() !== CoverageType.CSS) {
        continue;
      }
      const contentProvider = entry.contentProvider() as SDK.CSSStyleSheetHeader.CSSStyleSheetHeader;
      this._coverageByContentProvider.delete(contentProvider);
      const key = `${contentProvider.startLine}:${contentProvider.startColumn}`;
      const urlEntry = this._coverageByURL.get(entry.url());
      if (!urlEntry || !urlEntry._coverageInfoByLocation.delete(key)) {
        continue;
      }
      urlEntry._addToSizes(-entry._usedSize, -entry._size);
      if (!urlEntry._coverageInfoByLocation.size) {
        this._coverageByURL.delete(entry.url());
      }
    }

    if (this._cssModel) {
      for (const styleSheetHeader of this._cssModel.getAllStyleSheetHeaders()) {
        this._addStyleSheetToCSSCoverage(styleSheetHeader);
      }
    }
  }

  async _takeAllCoverage(): Promise<CoverageInfo[]> {
    const [updatesCSS, updatesJS] = await Promise.all([this._takeCSSCoverage(), this._takeJSCoverage()]);
    return [...updatesCSS, ...updatesJS];
  }

  async _takeJSCoverage(): Promise<CoverageInfo[]> {
    if (!this._cpuProfilerModel) {
      return [];
    }
    const {coverage, timestamp} = await this._cpuProfilerModel.takePreciseCoverage();
    this._coverageUpdateTimes.add(timestamp);
    return this._backlogOrProcessJSCoverage(coverage, timestamp);
  }

  coverageUpdateTimes(): Set<number> {
    return this._coverageUpdateTimes;
  }

  async _backlogOrProcessJSCoverage(freshRawCoverageData: Protocol.Profiler.ScriptCoverage[], freshTimestamp: number):
      Promise<CoverageInfo[]> {
    if (freshRawCoverageData.length > 0) {
      this._jsBacklog.push({rawCoverageData: freshRawCoverageData, stamp: freshTimestamp});
    }
    if (this._suspensionState !== SuspensionState.Active) {
      return [];
    }
    const ascendingByTimestamp = (x: {stamp: number}, y: {stamp: number}): number => x.stamp - y.stamp;
    const results = [];
    for (const {rawCoverageData, stamp} of this._jsBacklog.sort(ascendingByTimestamp)) {
      results.push(this._processJSCoverage(rawCoverageData, stamp));
    }
    this._jsBacklog = [];
    return results.flat();
  }

  async processJSBacklog(): Promise<void> {
    this._backlogOrProcessJSCoverage([], 0);
  }

  _processJSCoverage(scriptsCoverage: Protocol.Profiler.ScriptCoverage[], stamp: number): CoverageInfo[] {
    if (!this._debuggerModel) {
      return [];
    }
    const updatedEntries = [];
    for (const entry of scriptsCoverage) {
      const script = this._debuggerModel.scriptForId(entry.scriptId);
      if (!script) {
        continue;
      }

      const ranges = [];
      let type = CoverageType.JavaScript;
      for (const func of entry.functions) {
        // Do not coerce undefined to false, i.e. only consider blockLevel to be false
        // if back-end explicitly provides blockLevel field, otherwise presume blockLevel
        // coverage is not available. Also, ignore non-block level functions that weren't
        // ever called.
        if (func.isBlockCoverage === false && !(func.ranges.length === 1 && !func.ranges[0].count)) {
          type |= CoverageType.JavaScriptPerFunction;
        }
        for (const range of func.ranges) {
          ranges.push(range);
        }
      }
      const subentry = this._addCoverage(
          script, script.contentLength, script.lineOffset, script.columnOffset, ranges, type as CoverageType, stamp);
      if (subentry) {
        updatedEntries.push(subentry);
      }
    }
    return updatedEntries;
  }

  _handleStyleSheetAdded(event: Common.EventTarget.EventTargetEvent): void {
    const styleSheetHeader = event.data as SDK.CSSStyleSheetHeader.CSSStyleSheetHeader;

    this._addStyleSheetToCSSCoverage(styleSheetHeader);
  }

  async _takeCSSCoverage(): Promise<CoverageInfo[]> {
    // Don't poll if we have no model, or are suspended.
    if (!this._cssModel || this._suspensionState !== SuspensionState.Active) {
      return [];
    }
    const {coverage, timestamp} = await this._cssModel.takeCoverageDelta();
    this._coverageUpdateTimes.add(timestamp);
    return this._backlogOrProcessCSSCoverage(coverage, timestamp);
  }

  async _backlogOrProcessCSSCoverage(freshRawCoverageData: Protocol.CSS.RuleUsage[], freshTimestamp: number):
      Promise<CoverageInfo[]> {
    if (freshRawCoverageData.length > 0) {
      this._cssBacklog.push({rawCoverageData: freshRawCoverageData, stamp: freshTimestamp});
    }
    if (this._suspensionState !== SuspensionState.Active) {
      return [];
    }
    const ascendingByTimestamp = (x: {stamp: number}, y: {stamp: number}): number => x.stamp - y.stamp;
    const results = [];
    for (const {rawCoverageData, stamp} of this._cssBacklog.sort(ascendingByTimestamp)) {
      results.push(this._processCSSCoverage(rawCoverageData, stamp));
    }
    this._cssBacklog = [];
    return results.flat();
  }

  _processCSSCoverage(ruleUsageList: Protocol.CSS.RuleUsage[], stamp: number): CoverageInfo[] {
    if (!this._cssModel) {
      return [];
    }
    const updatedEntries = [];
    const rulesByStyleSheet = new Map<SDK.CSSStyleSheetHeader.CSSStyleSheetHeader, RangeUseCount[]>();
    for (const rule of ruleUsageList) {
      const styleSheetHeader = this._cssModel.styleSheetHeaderForId(rule.styleSheetId);
      if (!styleSheetHeader) {
        continue;
      }
      let ranges = rulesByStyleSheet.get(styleSheetHeader);
      if (!ranges) {
        ranges = [];
        rulesByStyleSheet.set(styleSheetHeader, ranges);
      }
      ranges.push({startOffset: rule.startOffset, endOffset: rule.endOffset, count: Number(rule.used)});
    }
    for (const entry of rulesByStyleSheet) {
      const styleSheetHeader = entry[0] as SDK.CSSStyleSheetHeader.CSSStyleSheetHeader;
      const ranges = entry[1] as RangeUseCount[];
      const subentry = this._addCoverage(
          styleSheetHeader, styleSheetHeader.contentLength, styleSheetHeader.startLine, styleSheetHeader.startColumn,
          ranges, CoverageType.CSS, stamp);
      if (subentry) {
        updatedEntries.push(subentry);
      }
    }
    return updatedEntries;
  }

  static _convertToDisjointSegments(ranges: RangeUseCount[], stamp: number): CoverageSegment[] {
    ranges.sort((a, b) => a.startOffset - b.startOffset);

    const result: CoverageSegment[] = [];
    const stack = [];
    for (const entry of ranges) {
      let top: RangeUseCount = stack[stack.length - 1];
      while (top && top.endOffset <= entry.startOffset) {
        append(top.endOffset, top.count);
        stack.pop();
        top = stack[stack.length - 1];
      }
      append(entry.startOffset, top ? top.count : 0);
      stack.push(entry);
    }

    for (let top = stack.pop(); top; top = stack.pop()) {
      append(top.endOffset, top.count);
    }

    function append(end: number, count: number): void {
      const last = result[result.length - 1];
      if (last) {
        if (last.end === end) {
          return;
        }
        if (last.count === count) {
          last.end = end;
          return;
        }
      }
      result.push({end: end, count: count, stamp: stamp});
    }

    return result;
  }

  _addStyleSheetToCSSCoverage(styleSheetHeader: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader): void {
    this._addCoverage(
        styleSheetHeader, styleSheetHeader.contentLength, styleSheetHeader.startLine, styleSheetHeader.startColumn, [],
        CoverageType.CSS, Date.now());
  }

  _addCoverage(
      contentProvider: TextUtils.ContentProvider.ContentProvider, contentLength: number, startLine: number,
      startColumn: number, ranges: RangeUseCount[], type: CoverageType, stamp: number): CoverageInfo|null {
    const url = contentProvider.contentURL();
    if (!url) {
      return null;
    }
    let urlCoverage = this._coverageByURL.get(url);
    let isNewUrlCoverage = false;
    if (!urlCoverage) {
      isNewUrlCoverage = true;
      urlCoverage = new URLCoverageInfo(url);
      this._coverageByURL.set(url, urlCoverage);
    }

    const coverageInfo = urlCoverage._ensureEntry(contentProvider, contentLength, startLine, startColumn, type);
    this._coverageByContentProvider.set(contentProvider, coverageInfo);
    const segments = CoverageModel._convertToDisjointSegments(ranges, stamp);
    const last = segments[segments.length - 1];
    if (last && last.end < contentLength) {
      segments.push({end: contentLength, stamp: stamp, count: 0});
    }
    const oldUsedSize = coverageInfo._usedSize;
    coverageInfo.mergeCoverage(segments);
    if (!isNewUrlCoverage && coverageInfo._usedSize === oldUsedSize) {
      return null;
    }
    urlCoverage._addToSizes(coverageInfo._usedSize - oldUsedSize, 0);
    return coverageInfo;
  }

  async exportReport(fos: Bindings.FileUtils.FileOutputStream): Promise<void> {
    const result: {url: string, ranges: {start: number, end: number}[], text: string|null}[] = [];
    function locationCompare(a: string, b: string): number {
      const [aLine, aPos] = a.split(':');
      const [bLine, bPos] = b.split(':');
      return Number.parseInt(aLine, 10) - Number.parseInt(bLine, 10) ||
          Number.parseInt(aPos, 10) - Number.parseInt(bPos, 10);
    }
    const coverageByUrlKeys = Array.from(this._coverageByURL.keys()).sort();
    for (const urlInfoKey of coverageByUrlKeys) {
      const urlInfo = this._coverageByURL.get(urlInfoKey);
      if (!urlInfo) {
        continue;
      }
      const url = urlInfo.url();
      if (url.startsWith('extensions::') || url.startsWith('chrome-extension://')) {
        continue;
      }

      // For .html resources, multiple scripts share URL, but have different offsets.
      let useFullText = false;
      for (const info of urlInfo._coverageInfoByLocation.values()) {
        if (info._lineOffset || info._columnOffset) {
          useFullText = Boolean(url);
          break;
        }
      }

      let fullText: TextUtils.Text.Text|null = null;
      if (useFullText) {
        const resource = SDK.ResourceTreeModel.ResourceTreeModel.resourceForURL(url);
        if (resource) {
          const content = (await resource.requestContent()).content;
          fullText = new TextUtils.Text.Text(content || '');
        }
      }

      const coverageByLocationKeys = Array.from(urlInfo._coverageInfoByLocation.keys()).sort(locationCompare);

      // We have full text for this resource, resolve the offsets using the text line endings.
      if (fullText) {
        const entry: {
          url: string,
          ranges: {start: number, end: number}[],
          text: string,
        } = {url, ranges: [], text: fullText.value()};
        for (const infoKey of coverageByLocationKeys) {
          const info = urlInfo._coverageInfoByLocation.get(infoKey);
          if (!info) {
            continue;
          }
          const offset = fullText ? fullText.offsetFromPosition(info._lineOffset, info._columnOffset) : 0;
          let start = 0;
          for (const segment of info._segments) {
            if (segment.count) {
              entry.ranges.push({start: start + offset, end: segment.end + offset});
            } else {
              start = segment.end;
            }
          }
        }
        result.push(entry);
        continue;
      }

      // Fall back to the per-script operation.
      for (const infoKey of coverageByLocationKeys) {
        const info = urlInfo._coverageInfoByLocation.get(infoKey);
        if (!info) {
          continue;
        }
        const entry: {
          url: string,
          ranges: {start: number, end: number}[],
          text: string|null,
        } = {url, ranges: [], text: (await info.contentProvider().requestContent()).content};
        let start = 0;
        for (const segment of info._segments) {
          if (segment.count) {
            entry.ranges.push({start: start, end: segment.end});
          } else {
            start = segment.end;
          }
        }
        result.push(entry);
      }
    }
    await fos.write(JSON.stringify(result, undefined, 2));
    fos.close();
  }
}

SDK.SDKModel.SDKModel.register(CoverageModel, SDK.SDKModel.Capability.None, false);

export class URLCoverageInfo extends Common.ObjectWrapper.ObjectWrapper {
  _url: string;
  _coverageInfoByLocation: Map<string, CoverageInfo>;
  _size: number;
  _usedSize: number;
  _type!: CoverageType;
  _isContentScript: boolean;
  constructor(url: string) {
    super();

    this._url = url;
    this._coverageInfoByLocation = new Map();
    this._size = 0;
    this._usedSize = 0;
    this._isContentScript = false;
  }

  url(): string {
    return this._url;
  }

  type(): CoverageType {
    return this._type;
  }

  size(): number {
    return this._size;
  }

  usedSize(): number {
    return this._usedSize;
  }

  unusedSize(): number {
    return this._size - this._usedSize;
  }

  usedPercentage(): number {
    // Per convention, empty files are reported as 100 % uncovered
    if (this._size === 0) {
      return 0;
    }
    return this.usedSize() / this.size() * 100;
  }

  unusedPercentage(): number {
    // Per convention, empty files are reported as 100 % uncovered
    if (this._size === 0) {
      return 100;
    }
    return this.unusedSize() / this.size() * 100;
  }

  isContentScript(): boolean {
    return this._isContentScript;
  }

  entries(): IterableIterator<CoverageInfo> {
    return this._coverageInfoByLocation.values();
  }

  _addToSizes(usedSize: number, size: number): void {
    this._usedSize += usedSize;
    this._size += size;

    if (usedSize !== 0 || size !== 0) {
      this.dispatchEventToListeners(URLCoverageInfo.Events.SizesChanged);
    }
  }

  _ensureEntry(
      contentProvider: TextUtils.ContentProvider.ContentProvider, contentLength: number, lineOffset: number,
      columnOffset: number, type: CoverageType): CoverageInfo {
    const key = `${lineOffset}:${columnOffset}`;
    let entry = this._coverageInfoByLocation.get(key);

    if ((type & CoverageType.JavaScript) && !this._coverageInfoByLocation.size) {
      this._isContentScript = (contentProvider as SDK.Script.Script).isContentScript();
    }
    this._type |= type;

    if (entry) {
      entry._coverageType |= type;
      return entry;
    }

    if ((type & CoverageType.JavaScript) && !this._coverageInfoByLocation.size) {
      this._isContentScript = (contentProvider as SDK.Script.Script).isContentScript();
    }

    entry = new CoverageInfo(contentProvider, contentLength, lineOffset, columnOffset, type);
    this._coverageInfoByLocation.set(key, entry);
    this._addToSizes(0, contentLength);

    return entry;
  }
}

export namespace URLCoverageInfo {
  export const Events = {
    SizesChanged: Symbol('SizesChanged'),
  };
}

export const mergeSegments = (segmentsA: CoverageSegment[], segmentsB: CoverageSegment[]): CoverageSegment[] => {
  const result: CoverageSegment[] = [];

  let indexA = 0;
  let indexB = 0;
  while (indexA < segmentsA.length && indexB < segmentsB.length) {
    const a = segmentsA[indexA];
    const b = segmentsB[indexB];
    const count = (a.count || 0) + (b.count || 0);
    const end = Math.min(a.end, b.end);
    const last = result[result.length - 1];
    const stamp = Math.min(a.stamp, b.stamp);
    if (!last || last.count !== count || last.stamp !== stamp) {
      result.push({end: end, count: count, stamp: stamp});
    } else {
      last.end = end;
    }
    if (a.end <= b.end) {
      indexA++;
    }
    if (a.end >= b.end) {
      indexB++;
    }
  }

  for (; indexA < segmentsA.length; indexA++) {
    result.push(segmentsA[indexA]);
  }
  for (; indexB < segmentsB.length; indexB++) {
    result.push(segmentsB[indexB]);
  }
  return result;
};

export class CoverageInfo {
  _contentProvider: TextUtils.ContentProvider.ContentProvider;
  _size: number;
  _usedSize: number;
  _statsByTimestamp: Map<number, number>;
  _lineOffset: number;
  _columnOffset: number;
  _coverageType: CoverageType;
  _segments: CoverageSegment[];
  constructor(
      contentProvider: TextUtils.ContentProvider.ContentProvider, size: number, lineOffset: number,
      columnOffset: number, type: CoverageType) {
    this._contentProvider = contentProvider;
    this._size = size;
    this._usedSize = 0;
    this._statsByTimestamp = new Map();
    this._lineOffset = lineOffset;
    this._columnOffset = columnOffset;
    this._coverageType = type;

    this._segments = [];
  }

  contentProvider(): TextUtils.ContentProvider.ContentProvider {
    return this._contentProvider;
  }

  url(): string {
    return this._contentProvider.contentURL();
  }

  type(): CoverageType {
    return this._coverageType;
  }

  mergeCoverage(segments: CoverageSegment[]): void {
    this._segments = mergeSegments(this._segments, segments);
    this._updateStats();
  }

  usedByTimestamp(): Map<number, number> {
    return this._statsByTimestamp;
  }

  size(): number {
    return this._size;
  }

  usageForRange(start: number, end: number): boolean {
    let index =
        Platform.ArrayUtilities.upperBound(this._segments, start, (position, segment) => position - segment.end);
    for (; index < this._segments.length && this._segments[index].end < end; ++index) {
      if (this._segments[index].count) {
        return true;
      }
    }
    return index < this._segments.length && Boolean(this._segments[index].count);
  }
  _updateStats(): void {
    this._statsByTimestamp = new Map();
    this._usedSize = 0;

    let last = 0;
    for (const segment of this._segments) {
      let previousCount = this._statsByTimestamp.get(segment.stamp);
      if (previousCount === undefined) {
        previousCount = 0;
      }

      if (segment.count) {
        const used = segment.end - last;
        this._usedSize += used;
        this._statsByTimestamp.set(segment.stamp, previousCount + used);
      }
      last = segment.end;
    }
  }
}
export interface RangeUseCount {
  startOffset: number;
  endOffset: number;
  count: number;
}
export interface CoverageSegment {
  end: number;
  count: number;
  stamp: number;
}
