// Copyright 2014 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.
/**
 * @unrestricted
 * @implements {SDK.SDKModelObserver<!SDK.DebuggerModel>}
 */
Bindings.BlackboxManager = class {
  /**
   * @param {!Bindings.DebuggerWorkspaceBinding} debuggerWorkspaceBinding
   */
  constructor(debuggerWorkspaceBinding) {
    this._debuggerWorkspaceBinding = debuggerWorkspaceBinding;

    SDK.targetManager.addModelListener(
        SDK.DebuggerModel, SDK.DebuggerModel.Events.ParsedScriptSource, this._parsedScriptSource, this);
    SDK.targetManager.addModelListener(
        SDK.DebuggerModel, SDK.DebuggerModel.Events.GlobalObjectCleared, this._globalObjectCleared, this);
    Common.moduleSetting('skipStackFramesPattern').addChangeListener(this._patternChanged.bind(this));
    Common.moduleSetting('skipContentScripts').addChangeListener(this._patternChanged.bind(this));

    /** @type {!Set<function()>} */
    this._listeners = new Set();

    /** @type {!Map<!SDK.DebuggerModel, !Map<string, !Array<!Protocol.Debugger.ScriptPosition>>>} */
    this._debuggerModelData = new Map();
    /** @type {!Map<string, boolean>} */
    this._isBlackboxedURLCache = new Map();

    SDK.targetManager.observeModels(SDK.DebuggerModel, this);
  }

  /**
   * @param {function()} listener
   */
  addChangeListener(listener) {
    this._listeners.add(listener);
  }

  /**
   * @param {function()} listener
   */
  removeChangeListener(listener) {
    this._listeners.delete(listener);
  }

  /**
   * @override
   * @param {!SDK.DebuggerModel} debuggerModel
   */
  modelAdded(debuggerModel) {
    this._setBlackboxPatterns(debuggerModel);
  }

  /**
   * @override
   * @param {!SDK.DebuggerModel} debuggerModel
   */
  modelRemoved(debuggerModel) {
    this._debuggerModelData.delete(debuggerModel);
    this._isBlackboxedURLCache.clear();
  }

  /**
   * @param {!SDK.DebuggerModel} debuggerModel
   * @return {!Promise<boolean>}
   */
  _setBlackboxPatterns(debuggerModel) {
    const regexPatterns = Common.moduleSetting('skipStackFramesPattern').getAsArray();
    const patterns = /** @type {!Array<string>} */ ([]);
    for (const item of regexPatterns) {
      if (!item.disabled && item.pattern)
        patterns.push(item.pattern);
    }
    //fixed by xxxxxx
    patterns.push('js\-framework\.js$');
    patterns.push('app\-service\.js$');
    patterns.push('Runtime\.Android\.js$');
    patterns.push("^webpack://.*/index\.nvue\b");
    return debuggerModel.setBlackboxPatterns(patterns);
  }

  /**
   * @param {!SDK.DebuggerModel.Location} location
   * @return {boolean}
   */
  isBlackboxedRawLocation(location) {
    const script = location.script();
    if (!script)
      return false;
    const positions = this._scriptPositions(script);
    if (!positions)
      return this._isBlackboxedScript(script);
    const index = positions.lowerBound(location, comparator);
    return !!(index % 2);

    /**
     * @param {!SDK.DebuggerModel.Location} a
     * @param {!Protocol.Debugger.ScriptPosition} b
     * @return {number}
     */
    function comparator(a, b) {
      if (a.lineNumber !== b.lineNumber)
        return a.lineNumber - b.lineNumber;
      return a.columnNumber - b.columnNumber;
    }
  }

  /**
   * @param {!Workspace.UISourceCode} uiSourceCode
   * @return {boolean}
   */
  isBlackboxedUISourceCode(uiSourceCode) {
    const projectType = uiSourceCode.project().type();
    const isContentScript = projectType === Workspace.projectTypes.ContentScripts;
    if (isContentScript && Common.moduleSetting('skipContentScripts').get())
      return true;
    const url = this._uiSourceCodeURL(uiSourceCode);
    return url ? this.isBlackboxedURL(url) : false;
  }

  /**
   * @param {string} url
   * @param {boolean=} isContentScript
   * @return {boolean}
   */
  isBlackboxedURL(url, isContentScript) {
    if (this._isBlackboxedURLCache.has(url))
      return !!this._isBlackboxedURLCache.get(url);
    if (isContentScript && Common.moduleSetting('skipContentScripts').get())
      return true;
    const regex = Common.moduleSetting('skipStackFramesPattern').asRegExp();
    const isBlackboxed = regex && regex.test(url);
    this._isBlackboxedURLCache.set(url, isBlackboxed);
    return isBlackboxed;
  }

  /**
   * @param {!SDK.Script} script
   * @param {?SDK.SourceMap} sourceMap
   * @return {!Promise<undefined>}
   */
  sourceMapLoaded(script, sourceMap) {
    if (!sourceMap)
      return Promise.resolve();
    const previousScriptState = this._scriptPositions(script);
    if (!previousScriptState)
      return Promise.resolve();

    const hasBlackboxedMappings = sourceMap.sourceURLs().some(url => this.isBlackboxedURL(url));
    const mappings = hasBlackboxedMappings ? sourceMap.mappings().slice() : [];
    if (!mappings.length) {
      if (previousScriptState.length > 0)
        return this._setScriptState(script, []);
      return Promise.resolve();
    }
    mappings.sort(mappingComparator);

    let currentBlackboxed = false;
    let isBlackboxed = false;
    const positions = [];
    // If content in script file begin is not mapped and one or more ranges are blackboxed then blackbox it.
    if (mappings[0].lineNumber !== 0 || mappings[0].columnNumber !== 0) {
      positions.push({lineNumber: 0, columnNumber: 0});
      currentBlackboxed = true;
    }
    for (const mapping of mappings) {
      if (mapping.sourceURL && currentBlackboxed !== this.isBlackboxedURL(mapping.sourceURL)) {
        positions.push({lineNumber: mapping.lineNumber, columnNumber: mapping.columnNumber});
        currentBlackboxed = !currentBlackboxed;
      }
      isBlackboxed = currentBlackboxed || isBlackboxed;
    }
    return this._setScriptState(script, !isBlackboxed ? [] : positions);
    /**
     * @param {!SDK.SourceMapEntry} a
     * @param {!SDK.SourceMapEntry} b
     * @return {number}
     */
    function mappingComparator(a, b) {
      if (a.lineNumber !== b.lineNumber)
        return a.lineNumber - b.lineNumber;
      return a.columnNumber - b.columnNumber;
    }
  }

  /**
   * @param {!Workspace.UISourceCode} uiSourceCode
   * @return {?string}
   */
  _uiSourceCodeURL(uiSourceCode) {
    return uiSourceCode.project().type() === Workspace.projectTypes.Debugger ? null : uiSourceCode.url();
  }

  /**
   * @param {!Workspace.UISourceCode} uiSourceCode
   * @return {boolean}
   */
  canBlackboxUISourceCode(uiSourceCode) {
    const url = this._uiSourceCodeURL(uiSourceCode);
    return url ? !!this._urlToRegExpString(url) : false;
  }

  /**
   * @param {!Workspace.UISourceCode} uiSourceCode
   */
  blackboxUISourceCode(uiSourceCode) {
    const url = this._uiSourceCodeURL(uiSourceCode);
    if (url)
      this._blackboxURL(url);
  }

  /**
   * @param {!Workspace.UISourceCode} uiSourceCode
   */
  unblackboxUISourceCode(uiSourceCode) {
    const url = this._uiSourceCodeURL(uiSourceCode);
    if (url)
      this._unblackboxURL(url);
  }

  blackboxContentScripts() {
    Common.moduleSetting('skipContentScripts').set(true);
  }

  unblackboxContentScripts() {
    Common.moduleSetting('skipContentScripts').set(false);
  }

  /**
   * @param {string} url
   */
  _blackboxURL(url) {
    const regexPatterns = Common.moduleSetting('skipStackFramesPattern').getAsArray();
    const regexValue = this._urlToRegExpString(url);
    if (!regexValue)
      return;
    let found = false;
    for (let i = 0; i < regexPatterns.length; ++i) {
      const item = regexPatterns[i];
      if (item.pattern === regexValue) {
        item.disabled = false;
        found = true;
        break;
      }
    }
    if (!found)
      regexPatterns.push({pattern: regexValue});
    Common.moduleSetting('skipStackFramesPattern').setAsArray(regexPatterns);
  }

  /**
   * @param {string} url
   */
  _unblackboxURL(url) {
    let regexPatterns = Common.moduleSetting('skipStackFramesPattern').getAsArray();
    const regexValue = Bindings.blackboxManager._urlToRegExpString(url);
    if (!regexValue)
      return;
    regexPatterns = regexPatterns.filter(function(item) {
      return item.pattern !== regexValue;
    });
    for (let i = 0; i < regexPatterns.length; ++i) {
      const item = regexPatterns[i];
      if (item.disabled)
        continue;
      try {
        const regex = new RegExp(item.pattern);
        if (regex.test(url))
          item.disabled = true;
      } catch (e) {
      }
    }
    Common.moduleSetting('skipStackFramesPattern').setAsArray(regexPatterns);
  }

  _patternChanged() {
    this._isBlackboxedURLCache.clear();

    /** @type {!Array<!Promise>} */
    const promises = [];
    for (const debuggerModel of SDK.targetManager.models(SDK.DebuggerModel)) {
      promises.push(this._setBlackboxPatterns(debuggerModel));
      for (const script of debuggerModel.scripts())
        promises.push(this._addScript(script).then(loadSourceMap.bind(this, script)));
    }
    Promise.all(promises).then(() => {
      const listeners = Array.from(this._listeners);
      for (const listener of listeners)
        listener();
      this._patternChangeFinishedForTests();
    });

    /**
     * @param {!SDK.Script} script
     * @return {!Promise<undefined>}
     * @this {Bindings.BlackboxManager}
     */
    function loadSourceMap(script) {
      return this.sourceMapLoaded(script, this._debuggerWorkspaceBinding.sourceMapForScript(script));
    }
  }

  _patternChangeFinishedForTests() {
    // This method is sniffed in tests.
  }

  /**
   * @param {!Common.Event} event
   */
  _globalObjectCleared(event) {
    const debuggerModel = /** @type {!SDK.DebuggerModel} */ (event.data);
    this._debuggerModelData.delete(debuggerModel);
    this._isBlackboxedURLCache.clear();
  }

  /**
   * @param {!Common.Event} event
   */
  _parsedScriptSource(event) {
    const script = /** @type {!SDK.Script} */ (event.data);
    this._addScript(script);
  }

  /**
   * @param {!SDK.Script} script
   * @return {!Promise<undefined>}
   */
  _addScript(script) {
    if (!script.sourceURL && !script.sourceMapURL)
      return Promise.resolve();
    const blackboxed = this._isBlackboxedScript(script);
    return this._setScriptState(script, blackboxed ? [{lineNumber: 0, columnNumber: 0}] : []);
  }

  /**
   * @param {!SDK.Script} script
   * @return {boolean}
   */
  _isBlackboxedScript(script) {
    return this.isBlackboxedURL(script.sourceURL, script.isContentScript());
  }

  /**
   * @param {!SDK.Script} script
   * @return {?Array<!Protocol.Debugger.ScriptPosition>}
   */
  _scriptPositions(script) {
    if (this._debuggerModelData.has(script.debuggerModel))
      return this._debuggerModelData.get(script.debuggerModel).get(script.scriptId) || null;
    return null;
  }

  /**
   * @param {!SDK.Script} script
   * @param {!Array<!Protocol.Debugger.ScriptPosition>} positions
   */
  _setScriptPositions(script, positions) {
    const debuggerModel = script.debuggerModel;
    if (!this._debuggerModelData.has(debuggerModel))
      this._debuggerModelData.set(debuggerModel, new Map());
    this._debuggerModelData.get(debuggerModel).set(script.scriptId, positions);
  }

  /**
   * @param {!SDK.Script} script
   * @param {!Array<!Protocol.Debugger.ScriptPosition>} positions
   * @return {!Promise<undefined>}
   */
  _setScriptState(script, positions) {
    const previousScriptState = this._scriptPositions(script);
    if (previousScriptState) {
      let hasChanged = false;
      hasChanged = previousScriptState.length !== positions.length;
      for (let i = 0; !hasChanged && i < positions.length; ++i) {
        hasChanged = positions[i].lineNumber !== previousScriptState[i].lineNumber ||
            positions[i].columnNumber !== previousScriptState[i].columnNumber;
      }
      if (!hasChanged)
        return Promise.resolve();
    } else {
      if (positions.length === 0)
        return Promise.resolve().then(updateState.bind(this, false));
    }

    return script.setBlackboxedRanges(positions).then(updateState.bind(this));

    /**
     * @param {boolean} success
     * @this {Bindings.BlackboxManager}
     */
    function updateState(success) {
      if (success) {
        this._setScriptPositions(script, positions);
        this._debuggerWorkspaceBinding.updateLocations(script);
        const isBlackboxed = positions.length !== 0;
        if (!isBlackboxed && script.sourceMapURL)
          this._debuggerWorkspaceBinding.maybeLoadSourceMap(script);
      } else {
        const hasPositions = !!this._scriptPositions(script);
        if (!hasPositions)
          this._setScriptPositions(script, []);
      }
    }
  }

  /**
   * @param {string} url
   * @return {string}
   */
  _urlToRegExpString(url) {
    const parsedURL = new Common.ParsedURL(url);
    if (parsedURL.isAboutBlank() || parsedURL.isDataURL())
      return '';
    if (!parsedURL.isValid)
      return '^' + url.escapeForRegExp() + '$';
    let name = parsedURL.lastPathComponent;
    if (name)
      name = '/' + name;
    else if (parsedURL.folderPathComponents)
      name = parsedURL.folderPathComponents + '/';
    if (!name)
      name = parsedURL.host;
    if (!name)
      return '';
    const scheme = parsedURL.scheme;
    let prefix = '';
    if (scheme && scheme !== 'http' && scheme !== 'https') {
      prefix = '^' + scheme + '://';
      if (scheme === 'chrome-extension')
        prefix += parsedURL.host + '\\b';
      prefix += '.*';
    }
    return prefix + name.escapeForRegExp() + (url.endsWith(name) ? '$' : '\\b');
  }
};

/** @type {!Bindings.BlackboxManager} */
Bindings.blackboxManager;
