// Copyright 2017 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.

/**
 * @typedef {!{
 *    id: string,
 *    contentProvider: !Common.ContentProvider,
 *    line: number,
 *    column: number
 * }}
 */
Coverage.RawLocation;

Coverage.CoverageDecorationManager = class {
  /**
   * @param {!Coverage.CoverageModel} coverageModel
   */
  constructor(coverageModel) {
    this._coverageModel = coverageModel;
    /** @type {!Map<!Common.ContentProvider, ?TextUtils.Text>} */
    this._textByProvider = new Map();
    /** @type {!Multimap<!Common.ContentProvider, !Workspace.UISourceCode>} */
    this._uiSourceCodeByContentProvider = new Multimap();
    // TODO(crbug.com/720162): get rid of this, use bindings.
    /** @type {!WeakMap<!Workspace.UISourceCode, !Array<!SDK.CSSStyleSheetHeader>>} */
    this._documentUISouceCodeToStylesheets = new WeakMap();

    for (const uiSourceCode of Workspace.workspace.uiSourceCodes())
      uiSourceCode.addLineDecoration(0, Coverage.CoverageDecorationManager._decoratorType, this);
    Workspace.workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, this._onUISourceCodeAdded, this);
  }

  reset() {
    for (const uiSourceCode of Workspace.workspace.uiSourceCodes())
      uiSourceCode.removeDecorationsForType(Coverage.CoverageDecorationManager._decoratorType);
  }

  dispose() {
    this.reset();
    Workspace.workspace.removeEventListener(
        Workspace.Workspace.Events.UISourceCodeAdded, this._onUISourceCodeAdded, this);
  }

  /**
   * @param {!Array<!Coverage.CoverageInfo>} updatedEntries
   */
  update(updatedEntries) {
    for (const entry of updatedEntries) {
      for (const uiSourceCode of this._uiSourceCodeByContentProvider.get(entry.contentProvider())) {
        uiSourceCode.removeDecorationsForType(Coverage.CoverageDecorationManager._decoratorType);
        uiSourceCode.addLineDecoration(0, Coverage.CoverageDecorationManager._decoratorType, this);
      }
    }
  }

  /**
   * @param {!Workspace.UISourceCode} uiSourceCode
   * @return {!Promise<!Array<boolean>>}
   */
  async usageByLine(uiSourceCode) {
    const result = [];
    const sourceText = new TextUtils.Text(uiSourceCode.content() || '');
    await this._updateTexts(uiSourceCode, sourceText);
    const lineEndings = sourceText.lineEndings();
    for (let line = 0; line < sourceText.lineCount(); ++line) {
      const lineLength = lineEndings[line] - (line ? lineEndings[line - 1] : 0) - 1;
      if (!lineLength) {
        result.push(undefined);
        continue;
      }
      const startLocations = this._rawLocationsForSourceLocation(uiSourceCode, line, 0);
      const endLocations = this._rawLocationsForSourceLocation(uiSourceCode, line, lineLength);
      let used = undefined;
      for (let startIndex = 0, endIndex = 0; startIndex < startLocations.length; ++startIndex) {
        const start = startLocations[startIndex];
        while (endIndex < endLocations.length &&
               Coverage.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;
  }

  /**
   * @param {!Workspace.UISourceCode} uiSourceCode
   * @param {!TextUtils.Text} text
   * @return {!Promise}
   */
  _updateTexts(uiSourceCode, text) {
    const promises = [];
    for (let line = 0; line < text.lineCount(); ++line) {
      for (const entry of this._rawLocationsForSourceLocation(uiSourceCode, line, 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));
      }
    }
    return Promise.all(promises);
  }

  /**
   * @param {!Common.ContentProvider} contentProvider
   * @return {!Promise}
   */
  async _updateTextForProvider(contentProvider) {
    const content = await contentProvider.requestContent();
    this._textByProvider.set(contentProvider, new TextUtils.Text(content));
  }

  /**
   * @param {!Workspace.UISourceCode} uiSourceCode
   * @param {number} line
   * @param {number} column
   * @return {!Array<!Coverage.RawLocation>}
   */
  _rawLocationsForSourceLocation(uiSourceCode, line, column) {
    const result = [];
    const contentType = uiSourceCode.contentType();
    if (contentType.hasScripts()) {
      let location = Bindings.debuggerWorkspaceBinding.uiLocationToRawLocation(uiSourceCode, line, column);
      if (location && location.script()) {
        const script = location.script();
        if (script.isInlineScript() && contentType.isDocument()) {
          if (comparePositions(script.lineOffset, script.columnOffset, location.lineNumber, location.columnNumber) >
                  0 ||
              comparePositions(script.endLine, script.endColumn, location.lineNumber, location.columnNumber) <= 0) {
            location = null;
          } else {
            location.lineNumber -= script.lineOffset;
            if (!location.lineNumber)
              location.columnNumber -= script.columnOffset;
          }
        }
        if (location) {
          result.push({
            id: `js:${location.scriptId}`,
            contentProvider: location.script(),
            line: location.lineNumber,
            column: location.columnNumber
          });
        }
      }
    }
    if (contentType.isStyleSheet() || contentType.isDocument()) {
      const rawStyleLocations = contentType.isDocument() ?
          this._documentUILocationToCSSRawLocations(uiSourceCode, line, column) :
          Bindings.cssWorkspaceBinding.uiLocationToRawLocations(new Workspace.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: location.header(),
          line: location.lineNumber,
          column: location.columnNumber
        });
      }
    }
    result.sort(Coverage.CoverageDecorationManager._compareLocations);

    /**
     * @param {number} aLine
     * @param {number} aColumn
     * @param {number} bLine
     * @param {number} bColumn
     * @return {number}
     */
    function comparePositions(aLine, aColumn, bLine, bColumn) {
      return aLine - bLine || aColumn - bColumn;
    }
    return result;
  }

  /**
   * TODO(crbug.com/720162): get rid of this, use bindings.
   *
   * @param {!Workspace.UISourceCode} uiSourceCode
   * @param {number} line
   * @param {number} column
   * @return {!Array<!SDK.CSSLocation>}
   */
  _documentUILocationToCSSRawLocations(uiSourceCode, line, column) {
    let stylesheets = this._documentUISouceCodeToStylesheets.get(uiSourceCode);
    if (!stylesheets) {
      stylesheets = [];
      const cssModel = this._coverageModel.target().model(SDK.CSSModel);
      if (!cssModel)
        return [];
      for (const headerId of cssModel.styleSheetIdsForURL(uiSourceCode.url())) {
        const header = cssModel.styleSheetHeaderForId(headerId);
        if (header)
          stylesheets.push(header);
      }
      stylesheets.sort(stylesheetComparator);
      this._documentUISouceCodeToStylesheets.set(uiSourceCode, stylesheets);
    }
    const endIndex =
        stylesheets.upperBound(undefined, (unused, header) => line - header.startLine || column - header.startColumn);
    if (!endIndex)
      return [];
    const locations = [];
    const last = stylesheets[endIndex - 1];
    for (let index = endIndex - 1; index >= 0 && stylesheets[index].startLine === last.startLine &&
         stylesheets[index].startColumn === last.startColumn;
         --index)
      locations.push(new SDK.CSSLocation(stylesheets[index], line, column));

    return locations;
    /**
     * @param {!SDK.CSSStyleSheetHeader} a
     * @param {!SDK.CSSStyleSheetHeader} b
     * @return {number}
     */
    function stylesheetComparator(a, b) {
      return a.startLine - b.startLine || a.startColumn - b.startColumn || a.id.localeCompare(b.id);
    }
  }

  /**
   * @param {!Coverage.RawLocation} a
   * @param {!Coverage.RawLocation} b
   */
  static _compareLocations(a, b) {
    return a.id.localeCompare(b.id) || a.line - b.line || a.column - b.column;
  }

  /**
   * @param {!Common.Event} event
   */
  _onUISourceCodeAdded(event) {
    const uiSourceCode = /** @type !Workspace.UISourceCode */ (event.data);
    uiSourceCode.addLineDecoration(0, Coverage.CoverageDecorationManager._decoratorType, this);
  }
};

Coverage.CoverageDecorationManager._decoratorType = 'coverage';

/**
 * @implements {SourceFrame.LineDecorator}
 */
Coverage.CoverageView.LineDecorator = class {
  /**
   * @override
   * @param {!Workspace.UISourceCode} uiSourceCode
   * @param {!TextEditor.CodeMirrorTextEditor} textEditor
   */
  decorate(uiSourceCode, textEditor) {
    const decorations = uiSourceCode.decorationsForType(Coverage.CoverageDecorationManager._decoratorType);
    if (!decorations || !decorations.size) {
      textEditor.uninstallGutter(Coverage.CoverageView.LineDecorator._gutterType);
      return;
    }
    const decorationManager =
        /** @type {!Coverage.CoverageDecorationManager} */ (decorations.values().next().value.data());
    decorationManager.usageByLine(uiSourceCode).then(lineUsage => {
      textEditor.operation(() => this._innerDecorate(textEditor, lineUsage));
    });
  }

  /**
   * @param {!TextEditor.CodeMirrorTextEditor} textEditor
   * @param {!Array<boolean>} lineUsage
   */
  _innerDecorate(textEditor, lineUsage) {
    const gutterType = Coverage.CoverageView.LineDecorator._gutterType;
    textEditor.uninstallGutter(gutterType);
    textEditor.installGutter(gutterType, false);
    for (let line = 0; line < lineUsage.length; ++line) {
      // Do not decorate the line if we don't have data.
      if (typeof lineUsage[line] !== 'boolean')
        continue;
      const className = lineUsage[line] ? 'text-editor-coverage-used-marker' : 'text-editor-coverage-unused-marker';
      textEditor.setGutterDecoration(line, gutterType, createElementWithClass('div', className));
    }
  }
};

Coverage.CoverageView.LineDecorator._gutterType = 'CodeMirror-gutter-coverage';