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

import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as UI from '../../ui/legacy/legacy.js';

import blockedURLsPaneStyles from './blockedURLsPane.css.js';

const UIStrings = {
  /**
  *@description Text to enable blocking of network requests
  */
  enableNetworkRequestBlocking: 'Enable network request blocking',
  /**
  *@description Tooltip text that appears when hovering over the largeicon add button in the Blocked URLs Pane of the Network panel
  */
  addPattern: 'Add pattern',
  /**
  *@description Tooltip text that appears when hovering over the largeicon clear button in the Blocked URLs Pane of the Network panel
  */
  removeAllPatterns: 'Remove all patterns',
  /**
  *@description Accessible label for the button to add request blocking patterns in the network request blocking tool
  */
  addNetworkRequestBlockingPattern: 'Add network request blocking pattern',
  /**
  *@description Button to add a pattern to block netwrok requests in the Network request blocking tool
  *@example {Add pattern} PH1
  */
  networkRequestsAreNotBlockedS: 'Network requests are not blocked. {PH1}',
  /**
  *@description Text in Blocked URLs Pane of the Network panel
  *@example {4} PH1
  */
  dBlocked: '{PH1} blocked',
  /**
  *@description Text in Blocked URLs Pane of the Network panel
  */
  textPatternToBlockMatching: 'Text pattern to block matching requests; use * for wildcard',
  /**
  *@description Error text for empty list widget input in Request Blocking tool
  */
  patternInputCannotBeEmpty: 'Pattern input cannot be empty.',
  /**
  *@description Error text for duplicate list widget input in Request Blocking tool
  */
  patternAlreadyExists: 'Pattern already exists.',
  /**
  *@description Message to be announced for a when list item is removed from list widget
  */
  itemDeleted: 'Item successfully deleted',
};
const str_ = i18n.i18n.registerUIStrings('panels/network/BlockedURLsPane.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export let blockedURLsPaneInstance: BlockedURLsPane|null = null;

export class BlockedURLsPane extends UI.Widget.VBox implements
    UI.ListWidget.Delegate<SDK.NetworkManager.BlockedPattern> {
  private manager: SDK.NetworkManager.MultitargetNetworkManager;
  private readonly toolbar: UI.Toolbar.Toolbar;
  private readonly enabledCheckbox: UI.Toolbar.ToolbarCheckbox;
  private readonly list: UI.ListWidget.ListWidget<SDK.NetworkManager.BlockedPattern>;
  private editor: UI.ListWidget.Editor<SDK.NetworkManager.BlockedPattern>|null;
  private blockedCountForUrl: Map<string, number>;
  private readonly updateThrottler: Common.Throttler.Throttler;

  constructor() {
    super(true);

    this.manager = SDK.NetworkManager.MultitargetNetworkManager.instance();
    this.manager.addEventListener(SDK.NetworkManager.MultitargetNetworkManager.Events.BlockedPatternsChanged, () => {
      void this.update();
    }, this);

    this.toolbar = new UI.Toolbar.Toolbar('', this.contentElement);
    this.enabledCheckbox = new UI.Toolbar.ToolbarCheckbox(
        i18nString(UIStrings.enableNetworkRequestBlocking), undefined, this.toggleEnabled.bind(this));
    this.toolbar.appendToolbarItem(this.enabledCheckbox);
    this.toolbar.appendSeparator();
    const addButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.addPattern), 'largeicon-add');
    addButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this.addButtonClicked, this);
    this.toolbar.appendToolbarItem(addButton);
    const clearButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.removeAllPatterns), 'largeicon-clear');
    clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this.removeAll, this);
    this.toolbar.appendToolbarItem(clearButton);

    this.list = new UI.ListWidget.ListWidget(this);
    this.list.element.classList.add('blocked-urls');

    this.list.setEmptyPlaceholder(this.createEmptyPlaceholder());
    this.list.show(this.contentElement);

    this.editor = null;

    this.blockedCountForUrl = new Map();
    SDK.TargetManager.TargetManager.instance().addModelListener(
        SDK.NetworkManager.NetworkManager, SDK.NetworkManager.Events.RequestFinished, this.onRequestFinished, this);

    this.updateThrottler = new Common.Throttler.Throttler(200);

    void this.update();
  }

  static instance(opts: {
    forceNew: boolean|null,
  } = {forceNew: null}): BlockedURLsPane {
    const {forceNew} = opts;
    if (!blockedURLsPaneInstance || forceNew) {
      blockedURLsPaneInstance = new BlockedURLsPane();
    }
    return blockedURLsPaneInstance;
  }

  private createEmptyPlaceholder(): Element {
    const element = this.contentElement.createChild('div', 'no-blocked-urls');
    const addButton =
        UI.UIUtils.createTextButton(i18nString(UIStrings.addPattern), this.addButtonClicked.bind(this), 'add-button');
    UI.ARIAUtils.setAccessibleName(addButton, i18nString(UIStrings.addNetworkRequestBlockingPattern));
    element.appendChild(
        i18n.i18n.getFormatLocalizedString(str_, UIStrings.networkRequestsAreNotBlockedS, {PH1: addButton}));
    return element;
  }

  static reset(): void {
    if (blockedURLsPaneInstance) {
      blockedURLsPaneInstance.reset();
    }
  }

  private addButtonClicked(): void {
    this.manager.setBlockingEnabled(true);
    this.list.addNewItem(0, {url: Platform.DevToolsPath.EmptyUrlString, enabled: true});
  }

  renderItem(pattern: SDK.NetworkManager.BlockedPattern, editable: boolean): Element {
    const count = this.blockedRequestsCount(pattern.url);
    const element = document.createElement('div');
    element.classList.add('blocked-url');
    const checkbox = (element.createChild('input', 'blocked-url-checkbox') as HTMLInputElement);
    checkbox.type = 'checkbox';
    checkbox.checked = pattern.enabled;
    checkbox.disabled = !editable;
    element.createChild('div', 'blocked-url-label').textContent = pattern.url;
    element.createChild('div', 'blocked-url-count').textContent = i18nString(UIStrings.dBlocked, {PH1: count});
    if (editable) {
      element.addEventListener('click', event => this.togglePattern(pattern, event));
      checkbox.addEventListener('click', event => this.togglePattern(pattern, event));
    }
    return element;
  }

  private togglePattern(pattern: SDK.NetworkManager.BlockedPattern, event: Event): void {
    event.consume(true);
    const patterns = this.manager.blockedPatterns();
    patterns.splice(patterns.indexOf(pattern), 1, {enabled: !pattern.enabled, url: pattern.url});
    this.manager.setBlockedPatterns(patterns);
  }

  private toggleEnabled(): void {
    this.manager.setBlockingEnabled(!this.manager.blockingEnabled());
    void this.update();
  }

  removeItemRequested(pattern: SDK.NetworkManager.BlockedPattern, index: number): void {
    const patterns = this.manager.blockedPatterns();
    patterns.splice(index, 1);
    this.manager.setBlockedPatterns(patterns);
    UI.ARIAUtils.alert(UIStrings.itemDeleted);
  }

  beginEdit(pattern: SDK.NetworkManager.BlockedPattern): UI.ListWidget.Editor<SDK.NetworkManager.BlockedPattern> {
    this.editor = this.createEditor();
    this.editor.control('url').value = pattern.url;
    return this.editor;
  }

  commitEdit(
      item: SDK.NetworkManager.BlockedPattern, editor: UI.ListWidget.Editor<SDK.NetworkManager.BlockedPattern>,
      isNew: boolean): void {
    const url = editor.control('url').value as Platform.DevToolsPath.UrlString;
    const patterns = this.manager.blockedPatterns();
    if (isNew) {
      patterns.push({enabled: true, url: url});
    } else {
      patterns.splice(patterns.indexOf(item), 1, {enabled: true, url: url});
    }

    this.manager.setBlockedPatterns(patterns);
  }

  private createEditor(): UI.ListWidget.Editor<SDK.NetworkManager.BlockedPattern> {
    if (this.editor) {
      return this.editor;
    }

    const editor = new UI.ListWidget.Editor<SDK.NetworkManager.BlockedPattern>();
    const content = editor.contentElement();
    const titles = content.createChild('div', 'blocked-url-edit-row');
    titles.createChild('div').textContent = i18nString(UIStrings.textPatternToBlockMatching);
    const fields = content.createChild('div', 'blocked-url-edit-row');
    const validator = (_item: SDK.NetworkManager.BlockedPattern, _index: number, input: UI.ListWidget.EditorControl): {
      valid: boolean,
      errorMessage: Common.UIString.LocalizedString|undefined,
    } => {
      let valid = true;
      let errorMessage;
      if (!input.value) {
        errorMessage = i18nString(UIStrings.patternInputCannotBeEmpty);
        valid = false;
      } else if (this.manager.blockedPatterns().find(pattern => pattern.url === input.value)) {
        errorMessage = i18nString(UIStrings.patternAlreadyExists);
        valid = false;
      }
      return {valid, errorMessage};
    };
    const urlInput = editor.createInput('url', 'text', '', validator);
    fields.createChild('div', 'blocked-url-edit-value').appendChild(urlInput);
    return editor;
  }

  private removeAll(): void {
    this.manager.setBlockedPatterns([]);
  }

  private update(): Promise<void> {
    const enabled = this.manager.blockingEnabled();
    this.list.element.classList.toggle('blocking-disabled', !enabled && Boolean(this.manager.blockedPatterns().length));

    this.enabledCheckbox.setChecked(enabled);
    this.list.clear();
    for (const pattern of this.manager.blockedPatterns()) {
      this.list.appendItem(pattern, enabled);
    }
    return Promise.resolve();
  }

  private blockedRequestsCount(url: string): number {
    if (!url) {
      return 0;
    }

    let result = 0;
    for (const blockedUrl of this.blockedCountForUrl.keys()) {
      if (this.matches(url, blockedUrl)) {
        result += (this.blockedCountForUrl.get(blockedUrl) as number);
      }
    }
    return result;
  }

  private matches(pattern: string, url: string): boolean {
    let pos = 0;
    const parts = pattern.split('*');
    for (let index = 0; index < parts.length; index++) {
      const part = parts[index];
      if (!part.length) {
        continue;
      }
      pos = url.indexOf(part, pos);
      if (pos === -1) {
        return false;
      }
      pos += part.length;
    }
    return true;
  }

  reset(): void {
    this.blockedCountForUrl.clear();
    void this.updateThrottler.schedule(this.update.bind(this));
  }

  private onRequestFinished(event: Common.EventTarget.EventTargetEvent<SDK.NetworkRequest.NetworkRequest>): void {
    const request = event.data;
    if (request.wasBlocked()) {
      const count = this.blockedCountForUrl.get(request.url()) || 0;
      this.blockedCountForUrl.set(request.url(), count + 1);
      void this.updateThrottler.schedule(this.update.bind(this));
    }
  }
  wasShown(): void {
    super.wasShown();
    this.list.registerCSSFiles([blockedURLsPaneStyles]);
    this.registerCSSFiles([blockedURLsPaneStyles]);
  }
}
