// 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 type * as Common from '../../core/common/common.js';
import * as Platform from '../../core/platform/platform.js';
import type * as Bindings from '../../models/bindings/bindings.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as Workspace from '../../models/workspace/workspace.js';

import type {CoverageInfo, CoverageModel} from './CoverageModel.js';

export const decoratorType = 'coverage';

export class CoverageDecorationManager {
  private coverageModel: CoverageModel;
  private readonly textByProvider: Map<TextUtils.ContentProvider.ContentProvider, TextUtils.Text.Text|null>;
  private readonly uiSourceCodeByContentProvider:
      Platform.MapUtilities.Multimap<TextUtils.ContentProvider.ContentProvider, Workspace.UISourceCode.UISourceCode>;

  readonly #workspace: Workspace.Workspace.WorkspaceImpl;
  readonly #debuggerBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding;
  readonly #cssBinding: Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding;

  constructor(
      coverageModel: CoverageModel, workspace: Workspace.Workspace.WorkspaceImpl,
      debuggerBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding,
      cssBinding: Bindings.CSSWorkspaceBinding.CSSWorkspaceBinding) {
    this.coverageModel = coverageModel;
    this.#workspace = workspace;
    this.#debuggerBinding = debuggerBinding;
    this.#cssBinding = cssBinding;

    this.textByProvider = new Map();
    this.uiSourceCodeByContentProvider = new Platform.MapUtilities.Multimap();

    for (const uiSourceCode of this.#workspace.uiSourceCodes()) {
      uiSourceCode.setDecorationData(decoratorType, this);
    }
    this.#workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, this.onUISourceCodeAdded, this);
  }

  reset(): void {
    for (const uiSourceCode of this.#workspace.uiSourceCodes()) {
      uiSourceCode.setDecorationData(decoratorType, undefined);
    }
  }

  dispose(): void {
    this.reset();
    this.#workspace.removeEventListener(Workspace.Workspace.Events.UISourceCodeAdded, this.onUISourceCodeAdded, this);
  }

  update(updatedEntries: CoverageInfo[]): void {
    for (const entry of updatedEntries) {
      for (const uiSourceCode of this.uiSourceCodeByContentProvider.get(entry.getContentProvider())) {
        uiSourceCode.setDecorationData(decoratorType, this);
      }
    }
  }

  /**
   * Returns the coverage per line of the provided uiSourceCode. The resulting array has the same length
   * as the provided `lines` array.
   *
   * @param uiSourceCode The UISourceCode for which to get the coverage info.
   * @param lineMappings The caller might have applied formatting to the UISourceCode. Each entry
   *                     in this array represents one line and the range specifies where it's found in
   *                     the original content.
   */
  async usageByLine(uiSourceCode: Workspace.UISourceCode.UISourceCode, lineMappings: TextUtils.TextRange.TextRange[]):
      Promise<Array<boolean|undefined>> {
    const result = [];
    await this.updateTexts(uiSourceCode, lineMappings);

    for (const {startLine, startColumn, endLine, endColumn} of lineMappings) {
      const startLocationsPromise = this.rawLocationsForSourceLocation(uiSourceCode, startLine, startColumn);
      const endLocationsPromise = this.rawLocationsForSourceLocation(uiSourceCode, endLine, endColumn);
      const [startLocations, endLocations] = await Promise.all([startLocationsPromise, endLocationsPromise]);
      let used: (boolean|undefined)|undefined = undefined;
      for (let startIndex = 0, endIndex = 0; startIndex < startLocations.length; ++startIndex) {
        const start = startLocations[startIndex];
        while (endIndex < endLocations.length &&
               CoverageDecorationManager.compareLocations(start, endLocations[endIndex]) >= 0) {
          ++endIndex;
        }
        if (endIndex >= endLocations.length || endLocations[endIndex].id !== start.id) {
          continue;
        }
        const end = endLocations[endIndex++];
        const text = this.textByProvider.get(end.contentProvider);
        if (!text) {
          continue;
        }
        const textValue = text.value();
        let startOffset = Math.min(text.offsetFromPosition(start.line, start.column), textValue.length - 1);
        let endOffset = Math.min(text.offsetFromPosition(end.line, end.column), textValue.length - 1);
        while (startOffset <= endOffset && /\s/.test(textValue[startOffset])) {
          ++startOffset;
        }
        while (startOffset <= endOffset && /\s/.test(textValue[endOffset])) {
          --endOffset;
        }
        if (startOffset <= endOffset) {
          used = this.coverageModel.usageForRange(end.contentProvider, startOffset, endOffset);
        }
        if (used) {
          break;
        }
      }
      result.push(used);
    }
    return result;
  }

  private async updateTexts(
      uiSourceCode: Workspace.UISourceCode.UISourceCode, lineMappings: TextUtils.TextRange.TextRange[]): Promise<void> {
    const promises = [];
    for (const range of lineMappings) {
      for (const entry of await this.rawLocationsForSourceLocation(uiSourceCode, range.startLine, 0)) {
        if (this.textByProvider.has(entry.contentProvider)) {
          continue;
        }
        this.textByProvider.set(entry.contentProvider, null);
        this.uiSourceCodeByContentProvider.set(entry.contentProvider, uiSourceCode);
        promises.push(this.updateTextForProvider(entry.contentProvider));
      }
    }
    await Promise.all(promises);
  }

  private async updateTextForProvider(contentProvider: TextUtils.ContentProvider.ContentProvider): Promise<void> {
    const contentData =
        TextUtils.ContentData.ContentData.contentDataOrEmpty(await contentProvider.requestContentData());
    this.textByProvider.set(contentProvider, contentData.textObj);
  }

  private async rawLocationsForSourceLocation(
      uiSourceCode: Workspace.UISourceCode.UISourceCode, line: number, column: number): Promise<RawLocation[]> {
    const result: RawLocation[] = [];
    const contentType = uiSourceCode.contentType();
    if (contentType.hasScripts()) {
      let locations = await this.#debuggerBinding.uiLocationToRawLocations(uiSourceCode, line, column);
      locations = locations.filter(location => !!location.script());
      for (const location of locations) {
        const script = location.script();
        if (!script) {
          continue;
        }
        if (script.isInlineScript() && contentType.isDocument()) {
          location.lineNumber -= script.lineOffset;
          if (!location.lineNumber) {
            location.columnNumber -= script.columnOffset;
          }
        }
        result.push({
          id: `js:${location.scriptId}`,
          contentProvider: script,
          line: location.lineNumber,
          column: location.columnNumber,
        });
      }
    }
    if (contentType.isStyleSheet() || contentType.isDocument()) {
      const rawStyleLocations =
          this.#cssBinding.uiLocationToRawLocations(new Workspace.UISourceCode.UILocation(uiSourceCode, line, column));
      for (const location of rawStyleLocations) {
        const header = location.header();
        if (!header) {
          continue;
        }
        if (header.isInline && contentType.isDocument()) {
          location.lineNumber -= header.startLine;
          if (!location.lineNumber) {
            location.columnNumber -= header.startColumn;
          }
        }
        result.push({
          id: `css:${location.styleSheetId}`,
          contentProvider: header,
          line: location.lineNumber,
          column: location.columnNumber,
        });
      }
    }
    return result.sort(CoverageDecorationManager.compareLocations);
  }

  private static compareLocations(a: RawLocation, b: RawLocation): number {
    return a.id.localeCompare(b.id) || a.line - b.line || a.column - b.column;
  }

  private onUISourceCodeAdded(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
    const uiSourceCode = event.data;
    uiSourceCode.setDecorationData(decoratorType, this);
  }
}
export interface RawLocation {
  id: string;
  contentProvider: TextUtils.ContentProvider.ContentProvider;
  line: number;
  column: number;
}
