text: /*
   * Copyright (C) 2013 Google Inc. All rights reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions are
   * met:
   *
   *     * Redistributions of source code must retain the above copyright
   * notice, this list of conditions and the following disclaimer.
   *     * Redistributions in binary form must reproduce the above
   * copyright notice, this list of conditions and the following disclaimer
   * in the documentation and/or other materials provided with the
   * distribution.
   *     * Neither the name of Google Inc. nor the names of its
   * contributors may be used to endorse or promote products derived from
   * this software without specific prior written permission.
   *
   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
   * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
   * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
   * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   */
  
  import * as Common from '../common/common.js';
  import * as Host from '../host/host.js';
  
  import * as ARIAUtils from './ARIAUtils.js';
  import {Icon} from './Icon.js';
  import {KeyboardShortcut, Modifiers} from './KeyboardShortcut.js';
  import {bindCheckbox} from './SettingsUI.js';
  import {Suggestions} from './SuggestBox.js';  // eslint-disable-line no-unused-vars
  import {Events, TextPrompt} from './TextPrompt.js';
  import {ToolbarButton, ToolbarSettingToggle} from './Toolbar.js';  // eslint-disable-line no-unused-vars
  import {Tooltip} from './Tooltip.js';
  import {CheckboxLabel, createTextChild} from './UIUtils.js';
  import {HBox} from './Widget.js';
  
  /**
   * @unrestricted
   */
  export class FilterBar extends HBox {
    /**
     * @param {string} name
     * @param {boolean=} visibleByDefault
     */
    constructor(name, visibleByDefault) {
      super();
      this.registerRequiredCSS('ui/filter.css', {enableLegacyPatching: true});
      this._enabled = true;
      this.element.classList.add('filter-bar');
  
      this._stateSetting =
          Common.Settings.Settings.instance().createSetting('filterBar-' + name + '-toggled', !!visibleByDefault);
      this._filterButton =
          new ToolbarSettingToggle(this._stateSetting, 'largeicon-filter', Common.UIString.UIString('Filter'));
  
      /** @type {!Array<!FilterUI>} */
      this._filters = [];
  
      this._updateFilterBar();
      this._stateSetting.addChangeListener(this._updateFilterBar.bind(this));
    }
  
    /**
     * @return {!ToolbarButton}
     */
    filterButton() {
      return this._filterButton;
    }
  
    /**
     * @param {!FilterUI} filter
     */
    addFilter(filter) {
      this._filters.push(filter);
      this.element.appendChild(filter.element());
      filter.addEventListener(FilterUI.Events.FilterChanged, this._filterChanged, this);
      this._updateFilterButton();
    }
  
    /** @param {boolean} enabled */
    setEnabled(enabled) {
      this._enabled = enabled;
      this._filterButton.setEnabled(enabled);
      this._updateFilterBar();
    }
  
    forceShowFilterBar() {
      this._alwaysShowFilters = true;
      this._updateFilterBar();
    }
  
    showOnce() {
      this._stateSetting.set(true);
    }
  
    /**
     * @param {!Common.EventTarget.EventTargetEvent} event
     */
    _filterChanged(event) {
      this._updateFilterButton();
      this.dispatchEventToListeners(FilterBar.Events.Changed);
    }
  
    /**
     * @override
     */
    wasShown() {
      super.wasShown();
      this._updateFilterBar();
    }
  
    _updateFilterBar() {
      if (!this.parentWidget() || this._showingWidget) {
        return;
      }
      if (this.visible()) {
        this._showingWidget = true;
        this.showWidget();
        this._showingWidget = false;
      } else {
        this.hideWidget();
      }
    }
  
    /**
     * @override
     */
    focus() {
      for (let i = 0; i < this._filters.length; ++i) {
        if (this._filters[i] instanceof TextFilterUI) {
          const textFilterUI = /** @type {!TextFilterUI} */ (this._filters[i]);
          textFilterUI.focus();
          break;
        }
      }
    }
  
    _updateFilterButton() {
      let isActive = false;
      for (const filter of this._filters) {
        isActive = isActive || filter.isActive();
      }
      this._filterButton.setDefaultWithRedColor(isActive);
      this._filterButton.setToggleWithRedColor(isActive);
    }
  
    clear() {
      this.element.removeChildren();
      this._filters = [];
      this._updateFilterButton();
    }
  
    setting() {
      return this._stateSetting;
    }
  
    visible() {
      return this._alwaysShowFilters || (this._stateSetting.get() && this._enabled);
    }
  }
  
  FilterBar.Events = {
    Changed: Symbol('Changed'),
  };
  
  /**
   * @interface
   */
  export class FilterUI extends Common.EventTarget.EventTarget {
    /**
     * @return {boolean}
     */
    isActive() {
      throw new Error('not implemented');
    }
  
    /**
     * @return {!Element}
     */
    element() {
      throw new Error('not implemented');
    }
  }
  
  /** @enum {symbol} */
  FilterUI.Events = {
    FilterChanged: Symbol('FilterChanged')
  };
  
  /**
   * @implements {FilterUI}
   * @unrestricted
   */
  export class TextFilterUI extends Common.ObjectWrapper.ObjectWrapper {
    constructor() {
      super();
      this._filterElement = document.createElement('div');
      this._filterElement.className = 'filter-text-filter';
  
      const container = this._filterElement.createChild('div', 'filter-input-container');
      this._filterInputElement = container.createChild('span', 'filter-input-field');
  
      this._prompt = new TextPrompt();
      this._prompt.initialize(this._completions.bind(this), ' ');
      /** @type {!HTMLElement} */
      this._proxyElement = /** @type {!HTMLElement} */ (this._prompt.attach(this._filterInputElement));
      Tooltip.install(this._proxyElement, Common.UIString.UIString('e.g. /small[\\d]+/ url:a.com/b'));
      this._prompt.setPlaceholder(Common.UIString.UIString('Filter'));
      this._prompt.addEventListener(Events.TextChanged, this._valueChanged.bind(this));
  
      /** @type {?function(string, string, boolean=):!Promise<!Suggestions>} */
      this._suggestionProvider = null;
  
      const clearButton = container.createChild('div', 'filter-input-clear-button');
      clearButton.appendChild(Icon.create('mediumicon-gray-cross-hover', 'filter-cancel-button'));
      clearButton.addEventListener('click', () => {
        this.clear();
        this.focus();
      });
      this._updateEmptyStyles();
    }
  
    /**
     * @param {string} expression
     * @param {string} prefix
     * @param {boolean=} force
     * @return {!Promise<!Suggestions>}
     */
    _completions(expression, prefix, force) {
      if (this._suggestionProvider) {
        return this._suggestionProvider(expression, prefix, force);
      }
      return Promise.resolve([]);
    }
    /**
     * @override
     * @return {boolean}
     */
    isActive() {
      return !!this._prompt.text();
    }
  
    /**
     * @override
     * @return {!Element}
     */
    element() {
      return this._filterElement;
    }
  
    /**
     * @return {string}
     */
    value() {
      return this._prompt.textWithCurrentSuggestion();
    }
  
    /**
     * @param {string} value
     */
    setValue(value) {
      this._prompt.setText(value);
      this._valueChanged();
    }
  
    focus() {
      this._filterInputElement.focus();
    }
  
    /**
     * @param {(function(string, string, boolean=):!Promise<!Suggestions>)} suggestionProvider
     */
    setSuggestionProvider(suggestionProvider) {
      this._prompt.clearAutocomplete();
      this._suggestionProvider = suggestionProvider;
    }
  
    _valueChanged() {
      this.dispatchEventToListeners(FilterUI.Events.FilterChanged, null);
      this._updateEmptyStyles();
    }
  
    _updateEmptyStyles() {
      this._filterElement.classList.toggle('filter-text-empty', !this._prompt.text());
    }
  
    clear() {
      this.setValue('');
    }
  }
  
  /**
   * @implements {FilterUI}
   * @unrestricted
   */
  export class NamedBitSetFilterUI extends Common.ObjectWrapper.ObjectWrapper {
    /**
     * @param {!Array.<!Item>} items
     * @param {!Common.Settings.Setting<*>=} setting
     */
    constructor(items, setting) {
      super();
      this._filtersElement = document.createElement('div');
      this._filtersElement.classList.add('filter-bitset-filter');
      ARIAUtils.markAsListBox(this._filtersElement);
      ARIAUtils.markAsMultiSelectable(this._filtersElement);
      Tooltip.install(
          this._filtersElement,
          Common.UIString.UIString(
              '%sClick to select multiple types', KeyboardShortcut.shortcutToString('', Modifiers.CtrlOrMeta)));
  
      /** @type {!WeakMap<!HTMLElement, string>} */
      this._typeFilterElementTypeNames = new WeakMap();
      /** @type {!Set<string>} */
      this._allowedTypes = new Set();
      /** @type {!Array.<!HTMLElement>} */
      this._typeFilterElements = [];
      this._addBit(NamedBitSetFilterUI.ALL_TYPES, Common.UIString.UIString('All'));
      this._typeFilterElements[0].tabIndex = 0;
      this._filtersElement.createChild('div', 'filter-bitset-filter-divider');
  
      for (let i = 0; i < items.length; ++i) {
        this._addBit(items[i].name, items[i].label, items[i].title);
      }
  
      if (setting) {
        this._setting = setting;
        setting.addChangeListener(this._settingChanged.bind(this));
        this._settingChanged();
      } else {
        this._toggleTypeFilter(NamedBitSetFilterUI.ALL_TYPES, false /* allowMultiSelect */);
      }
    }
  
    reset() {
      this._toggleTypeFilter(NamedBitSetFilterUI.ALL_TYPES, false /* allowMultiSelect */);
    }
  
    /**
     * @override
     * @return {boolean}
     */
    isActive() {
      return !this._allowedTypes.has(NamedBitSetFilterUI.ALL_TYPES);
    }
  
    /**
     * @override
     * @return {!Element}
     */
    element() {
      return this._filtersElement;
    }
  
    /**
     * @param {string} typeName
     * @return {boolean}
     */
    accept(typeName) {
      return this._allowedTypes.has(NamedBitSetFilterUI.ALL_TYPES) || this._allowedTypes.has(typeName);
    }
  
    _settingChanged() {
      const allowedTypesFromSetting = /** @type {!Common.Settings.Setting<*>} */ (this._setting).get();
      this._allowedTypes = new Set();
      for (const element of this._typeFilterElements) {
        const typeName = this._typeFilterElementTypeNames.get(element);
        if (typeName && allowedTypesFromSetting[typeName]) {
          this._allowedTypes.add(typeName);
        }
      }
      this._update();
    }
  
    _update() {
      if (this._allowedTypes.size === 0 || this._allowedTypes.has(NamedBitSetFilterUI.ALL_TYPES)) {
        this._allowedTypes = new Set();
        this._allowedTypes.add(NamedBitSetFilterUI.ALL_TYPES);
      }
      for (const element of this._typeFilterElements) {
        const typeName = this._typeFilterElementTypeNames.get(element);
        const active = this._allowedTypes.has(typeName || '');
        element.classList.toggle('selected', active);
        ARIAUtils.setSelected(element, active);
      }
      this.dispatchEventToListeners(FilterUI.Events.FilterChanged, null);
    }
  
    /**
     * @param {string} name
     * @param {string} label
     * @param {string=} title
     */
    _addBit(name, label, title) {
      const typeFilterElement = /** @type {!HTMLElement} */ (this._filtersElement.createChild('span', name));
      typeFilterElement.tabIndex = -1;
      this._typeFilterElementTypeNames.set(typeFilterElement, name);
      createTextChild(typeFilterElement, label);
      ARIAUtils.markAsOption(typeFilterElement);
      if (title) {
        typeFilterElement.title = title;
      }
      typeFilterElement.addEventListener('click', this._onTypeFilterClicked.bind(this), false);
      typeFilterElement.addEventListener('keydown', this._onTypeFilterKeydown.bind(this), false);
      this._typeFilterElements.push(typeFilterElement);
    }
  
    /**
     * @param {!Event} event
     */
    _onTypeFilterClicked(event) {
      const e = /** @type {!KeyboardEvent} */ (event);
      let toggle;
      if (Host.Platform.isMac()) {
        toggle = e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey;
      } else {
        toggle = e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey;
      }
      if (e.target) {
        const element = /** @type {!HTMLElement} */ (e.target);
        const typeName = /** @type {string} */ (this._typeFilterElementTypeNames.get(element));
        this._toggleTypeFilter(typeName, toggle);
      }
    }
  
    /**
     * @param {!Event} ev
     */
    _onTypeFilterKeydown(ev) {
      const event = /** @type {!KeyboardEvent} */ (ev);
      const element = /** @type {?HTMLElement} */ (event.target);
      if (!element) {
        return;
      }
  
      if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
        if (this._keyFocusNextBit(element, true /* selectPrevious */)) {
          event.consume(true);
        }
      } else if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
        if (this._keyFocusNextBit(element, false /* selectPrevious */)) {
          event.consume(true);
        }
      } else if (isEnterOrSpaceKey(event)) {
        this._onTypeFilterClicked(event);
      }
    }
  
    /**
     * @param {!HTMLElement} target
     * @param {boolean} selectPrevious
     * @returns {!boolean}
     */
    _keyFocusNextBit(target, selectPrevious) {
      const index = this._typeFilterElements.indexOf(target);
      if (index === -1) {
        return false;
      }
      const nextIndex = selectPrevious ? index - 1 : index + 1;
      if (nextIndex < 0 || nextIndex >= this._typeFilterElements.length) {
        return false;
      }
  
      const nextElement = this._typeFilterElements[nextIndex];
      nextElement.tabIndex = 0;
      target.tabIndex = -1;
      nextElement.focus();
      return true;
    }
  
    /**
     * @param {string} typeName
     * @param {boolean} allowMultiSelect
     */
    _toggleTypeFilter(typeName, allowMultiSelect) {
      if (allowMultiSelect && typeName !== NamedBitSetFilterUI.ALL_TYPES) {
        this._allowedTypes.delete(NamedBitSetFilterUI.ALL_TYPES);
      } else {
        this._allowedTypes = new Set();
      }
  
      if (this._allowedTypes.has(typeName)) {
        this._allowedTypes.delete(typeName);
      } else {
        this._allowedTypes.add(typeName);
      }
  
      if (this._setting) {
        // Settings do not support `Sets` so convert it back to the Map-like object.
        const updatedSetting = /** @type {*} */ ({});
        for (const type of this._allowedTypes) {
          updatedSetting[type] = true;
        }
        this._setting.set(updatedSetting);
      } else {
        this._update();
      }
    }
  }
  
  NamedBitSetFilterUI.ALL_TYPES = 'all';
  
  /**
   * @implements {FilterUI}
   * @unrestricted
   */
  export class CheckboxFilterUI extends Common.ObjectWrapper.ObjectWrapper {
    /**
     * @param {string} className
     * @param {string} title
     * @param {boolean=} activeWhenChecked
     * @param {!Common.Settings.Setting<boolean>=} setting
     */
    constructor(className, title, activeWhenChecked, setting) {
      super();
      this._filterElement = /** @type {!HTMLDivElement} */ (document.createElement('div'));
      this._filterElement.classList.add('filter-checkbox-filter');
      this._activeWhenChecked = !!activeWhenChecked;
      this._label = CheckboxLabel.create(title);
      this._filterElement.appendChild(this._label);
      this._checkboxElement = this._label.checkboxElement;
      if (setting) {
        bindCheckbox(this._checkboxElement, setting);
      } else {
        this._checkboxElement.checked = true;
      }
      this._checkboxElement.addEventListener('change', this._fireUpdated.bind(this), false);
    }
  
    /**
     * @override
     * @return {boolean}
     */
    isActive() {
      return this._activeWhenChecked === this._checkboxElement.checked;
    }
  
    /**
     * @return {boolean}
     */
    checked() {
      return this._checkboxElement.checked;
    }
  
    /**
     * @param {boolean} checked
     */
    setChecked(checked) {
      this._checkboxElement.checked = checked;
    }
  
    /**
     * @override
     * @return {!HTMLDivElement}
     */
    element() {
      return this._filterElement;
    }
  
    /**
     * @return {!Element}
     */
    labelElement() {
      return this._label;
    }
  
    _fireUpdated() {
      this.dispatchEventToListeners(FilterUI.Events.FilterChanged, null);
    }
  
    /**
     * @param {string} backgroundColor
     * @param {string} borderColor
     */
    setColor(backgroundColor, borderColor) {
      this._label.backgroundColor = backgroundColor;
      this._label.borderColor = borderColor;
    }
  }
  
  /** @typedef {{name: string, label: string, title: (string|undefined)}} */
  // @ts-ignore typedef
  export let Item;
  