/*
 * Copyright (C) 2009 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 '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Root from '../../core/root/root.js';
import * as Utils from './utils/utils.js';

import type {Action} from './ActionRegistration.js';
import {Events as ActionEvents} from './ActionRegistration.js';
import {ActionRegistry} from './ActionRegistry.js';
import * as ARIAUtils from './ARIAUtils.js';
import {ContextMenu} from './ContextMenu.js';
import {GlassPane, PointerEventsBehavior} from './GlassPane.js';
import {Icon} from './Icon.js';
import {bindCheckbox} from './SettingsUI.js';
import type {Suggestion} from './SuggestBox.js';
import {Events as TextPromptEvents, TextPrompt} from './TextPrompt.js';
import {Tooltip} from './Tooltip.js';
import {CheckboxLabel, LongClickController} from './UIUtils.js';
import toolbarStyles from './toolbar.css.legacy.js';

const UIStrings = {
  /**
  *@description Announced screen reader message for ToolbarSettingToggle when the setting is toggled on.
  */
  pressed: 'pressed',
  /**
  *@description Announced screen reader message for ToolbarSettingToggle when the setting is toggled off.
  */
  notPressed: 'not pressed',
  /**
  *@description Tooltip shown when the user hovers over the clear icon to empty the text input.
  */
  clearInput: 'Clear input',
};
const str_ = i18n.i18n.registerUIStrings('ui/legacy/Toolbar.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export class Toolbar {
  private items: ToolbarItem[];
  element: HTMLElement;
  enabled: boolean;
  private readonly shadowRoot: ShadowRoot;
  private contentElement: Element;
  private insertionPoint: Element;
  private compactLayout = false;

  constructor(className: string, parentElement?: Element) {
    this.items = [];
    this.element = (parentElement ? parentElement.createChild('div') : document.createElement('div')) as HTMLElement;
    this.element.className = className;
    this.element.classList.add('toolbar');
    this.enabled = true;
    this.shadowRoot =
        Utils.createShadowRootWithCoreStyles(this.element, {cssFile: toolbarStyles, delegatesFocus: undefined});
    this.contentElement = this.shadowRoot.createChild('div', 'toolbar-shadow');
    this.insertionPoint = this.contentElement.createChild('slot');
  }

  hasCompactLayout(): boolean {
    return this.compactLayout;
  }

  registerCSSFiles(cssFiles: CSSStyleSheet[]): void {
    this.shadowRoot.adoptedStyleSheets = this.shadowRoot.adoptedStyleSheets.concat(cssFiles);
  }

  setCompactLayout(enable: boolean): void {
    if (this.compactLayout === enable) {
      return;
    }
    this.compactLayout = enable;
    for (const item of this.items) {
      item.setCompactLayout(enable);
    }
  }

  static createLongPressActionButton(
      action: Action, toggledOptions: ToolbarButton[], untoggledOptions: ToolbarButton[]): ToolbarButton {
    const button = Toolbar.createActionButton(action);
    const mainButtonClone = Toolbar.createActionButton(action);

    let longClickController: LongClickController|null = null;
    let longClickButtons: ToolbarButton[]|null = null;
    let longClickGlyph: Icon|null = null;

    action.addEventListener(ActionEvents.Toggled, updateOptions);
    updateOptions();
    return button;

    function updateOptions(): void {
      const buttons = action.toggled() ? (toggledOptions || null) : (untoggledOptions || null);

      if (buttons && buttons.length) {
        if (!longClickController) {
          longClickController = new LongClickController(button.element, showOptions);
          longClickGlyph = Icon.create('largeicon-longclick-triangle', 'long-click-glyph');
          button.element.appendChild(longClickGlyph);
          longClickButtons = buttons;
        }
      } else {
        if (longClickController) {
          longClickController.dispose();
          longClickController = null;
          if (longClickGlyph) {
            longClickGlyph.remove();
          }
          longClickGlyph = null;
          longClickButtons = null;
        }
      }
    }

    function showOptions(): void {
      let buttons: ToolbarButton[] = longClickButtons ? longClickButtons.slice() : [];
      buttons.push(mainButtonClone);

      const document = button.element.ownerDocument;
      document.documentElement.addEventListener('mouseup', mouseUp, false);

      const optionsGlassPane = new GlassPane();
      optionsGlassPane.setPointerEventsBehavior(PointerEventsBehavior.BlockedByGlassPane);
      optionsGlassPane.show(document);
      const optionsBar = new Toolbar('fill', optionsGlassPane.contentElement);
      optionsBar.contentElement.classList.add('floating');
      const buttonHeight = 26;

      const hostButtonPosition = button.element.boxInWindow().relativeToElement(GlassPane.container(document));

      const topNotBottom = hostButtonPosition.y + buttonHeight * buttons.length < document.documentElement.offsetHeight;

      if (topNotBottom) {
        buttons = buttons.reverse();
      }

      optionsBar.element.style.height = (buttonHeight * buttons.length) + 'px';
      if (topNotBottom) {
        optionsBar.element.style.top = (hostButtonPosition.y - 5) + 'px';
      } else {
        optionsBar.element.style.top = (hostButtonPosition.y - (buttonHeight * (buttons.length - 1)) - 6) + 'px';
      }
      optionsBar.element.style.left = (hostButtonPosition.x - 5) + 'px';

      for (let i = 0; i < buttons.length; ++i) {
        buttons[i].element.addEventListener('mousemove', mouseOver, false);
        buttons[i].element.addEventListener('mouseout', mouseOut, false);
        optionsBar.appendToolbarItem(buttons[i]);
      }
      const hostButtonIndex = topNotBottom ? 0 : buttons.length - 1;
      buttons[hostButtonIndex].element.classList.add('emulate-active');

      function mouseOver(e: Event): void {
        if ((e as MouseEvent).which !== 1) {
          return;
        }
        if (e.target instanceof HTMLElement) {
          const buttonElement = e.target.enclosingNodeOrSelfWithClass('toolbar-item');
          buttonElement.classList.add('emulate-active');
        }
      }

      function mouseOut(e: Event): void {
        if ((e as MouseEvent).which !== 1) {
          return;
        }
        if (e.target instanceof HTMLElement) {
          const buttonElement = e.target.enclosingNodeOrSelfWithClass('toolbar-item');
          buttonElement.classList.remove('emulate-active');
        }
      }

      function mouseUp(e: Event): void {
        if ((e as MouseEvent).which !== 1) {
          return;
        }
        optionsGlassPane.hide();
        document.documentElement.removeEventListener('mouseup', mouseUp, false);

        for (let i = 0; i < buttons.length; ++i) {
          if (buttons[i].element.classList.contains('emulate-active')) {
            buttons[i].element.classList.remove('emulate-active');
            buttons[i].clicked(e);
            break;
          }
        }
      }
    }
  }

  static createActionButton(action: Action, options: ToolbarButtonOptions|undefined = TOOLBAR_BUTTON_DEFAULT_OPTIONS):
      ToolbarButton {
    const button = action.toggleable() ? makeToggle() : makeButton();

    if (options.showLabel) {
      button.setText(action.title());
    }

    let handler = (_event: {
      // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      data: any,
    }): void => {
      void action.execute();
    };
    if (options.userActionCode) {
      const actionCode = options.userActionCode;
      handler = (): void => {
        Host.userMetrics.actionTaken(actionCode);
        void action.execute();
      };
    }
    button.addEventListener(ToolbarButton.Events.Click, handler, action);
    action.addEventListener(ActionEvents.Enabled, enabledChanged);
    button.setEnabled(action.enabled());
    return button;

    function makeButton(): ToolbarButton {
      const button = new ToolbarButton(action.title(), action.icon());
      if (action.title()) {
        Tooltip.installWithActionBinding(button.element, action.title(), action.id());
      }
      return button;
    }

    function makeToggle(): ToolbarToggle {
      const toggleButton = new ToolbarToggle(action.title(), action.icon(), action.toggledIcon());
      toggleButton.setToggleWithRedColor(action.toggleWithRedColor());
      action.addEventListener(ActionEvents.Toggled, toggled);
      toggled();
      return toggleButton;

      function toggled(): void {
        toggleButton.setToggled(action.toggled());
        if (action.title()) {
          toggleButton.setTitle(action.title());
          Tooltip.installWithActionBinding(toggleButton.element, action.title(), action.id());
        }
      }
    }

    function enabledChanged(event: Common.EventTarget.EventTargetEvent<boolean>): void {
      button.setEnabled(event.data);
    }
  }

  static createActionButtonForId(
      actionId: string, options: ToolbarButtonOptions|undefined = TOOLBAR_BUTTON_DEFAULT_OPTIONS): ToolbarButton {
    const action = ActionRegistry.instance().action(actionId);
    return Toolbar.createActionButton((action as Action), options);
  }

  gripElementForResize(): Element {
    return this.contentElement;
  }

  makeWrappable(growVertically?: boolean): void {
    this.contentElement.classList.add('wrappable');
    if (growVertically) {
      this.contentElement.classList.add('toolbar-grow-vertical');
    }
  }

  makeVertical(): void {
    this.contentElement.classList.add('vertical');
  }

  makeBlueOnHover(): void {
    this.contentElement.classList.add('toolbar-blue-on-hover');
  }

  makeToggledGray(): void {
    this.contentElement.classList.add('toolbar-toggled-gray');
  }

  renderAsLinks(): void {
    this.contentElement.classList.add('toolbar-render-as-links');
  }

  empty(): boolean {
    return !this.items.length;
  }

  setEnabled(enabled: boolean): void {
    this.enabled = enabled;
    for (const item of this.items) {
      item.applyEnabledState(this.enabled && item.enabled);
    }
  }

  appendToolbarItem(item: ToolbarItem): void {
    this.items.push(item);
    item.toolbar = this;
    item.setCompactLayout(this.hasCompactLayout());
    if (!this.enabled) {
      item.applyEnabledState(false);
    }
    this.contentElement.insertBefore(item.element, this.insertionPoint);
    this.hideSeparatorDupes();
  }

  appendSeparator(): void {
    this.appendToolbarItem(new ToolbarSeparator());
  }

  appendSpacer(): void {
    this.appendToolbarItem(new ToolbarSeparator(true));
  }

  appendText(text: string): void {
    this.appendToolbarItem(new ToolbarText(text));
  }

  removeToolbarItem(itemToRemove: ToolbarItem): void {
    const updatedItems = [];
    for (const item of this.items) {
      if (item === itemToRemove) {
        item.element.remove();
      } else {
        updatedItems.push(item);
      }
    }
    this.items = updatedItems;
  }

  removeToolbarItems(): void {
    for (const item of this.items) {
      item.toolbar = null;
    }
    this.items = [];
    this.contentElement.removeChildren();
    this.insertionPoint = this.contentElement.createChild('slot');
  }

  setColor(color: string): void {
    const style = document.createElement('style');
    style.textContent = '.toolbar-glyph { background-color: ' + color + ' !important }';
    this.shadowRoot.appendChild(style);
  }

  setToggledColor(color: string): void {
    const style = document.createElement('style');
    style.textContent =
        '.toolbar-button.toolbar-state-on .toolbar-glyph { background-color: ' + color + ' !important }';
    this.shadowRoot.appendChild(style);
  }

  hideSeparatorDupes(): void {
    if (!this.items.length) {
      return;
    }
    // Don't hide first and last separators if they were added explicitly.
    let previousIsSeparator = false;
    let lastSeparator;
    let nonSeparatorVisible = false;
    for (let i = 0; i < this.items.length; ++i) {
      if (this.items[i] instanceof ToolbarSeparator) {
        this.items[i].setVisible(!previousIsSeparator);
        previousIsSeparator = true;
        lastSeparator = this.items[i];
        continue;
      }
      if (this.items[i].visible()) {
        previousIsSeparator = false;
        lastSeparator = null;
        nonSeparatorVisible = true;
      }
    }
    if (lastSeparator && lastSeparator !== this.items[this.items.length - 1]) {
      lastSeparator.setVisible(false);
    }

    this.element.classList.toggle(
        'hidden',
        lastSeparator !== null && lastSeparator !== undefined && lastSeparator.visible() && !nonSeparatorVisible);
  }

  async appendItemsAtLocation(location: string): Promise<void> {
    const extensions: ToolbarItemRegistration[] = getRegisteredToolbarItems();

    extensions.sort((extension1, extension2) => {
      const order1 = extension1.order || 0;
      const order2 = extension2.order || 0;
      return order1 - order2;
    });

    const filtered = extensions.filter(e => e.location === location);
    const items = await Promise.all(filtered.map(extension => {
      const {separator, actionId, showLabel, loadItem} = extension;
      if (separator) {
        return new ToolbarSeparator();
      }
      if (actionId) {
        return Toolbar.createActionButtonForId(actionId, {showLabel: Boolean(showLabel), userActionCode: undefined});
      }
      // TODO(crbug.com/1134103) constratint the case checked with this if using TS type definitions once UI is TS-authored.
      if (!loadItem) {
        throw new Error('Could not load a toolbar item registration with no loadItem function');
      }
      return loadItem().then(p => (p as Provider).item());
    }));

    for (const item of items) {
      if (item) {
        this.appendToolbarItem(item);
      }
    }
  }
}
export interface ToolbarButtonOptions {
  showLabel: boolean;
  userActionCode?: Host.UserMetrics.Action;
}

const TOOLBAR_BUTTON_DEFAULT_OPTIONS: ToolbarButtonOptions = {
  showLabel: false,
  userActionCode: undefined,
};

// We need any here because Common.ObjectWrapper.ObjectWrapper is invariant in T.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class ToolbarItem<T = any> extends Common.ObjectWrapper.ObjectWrapper<T> {
  element: HTMLElement;
  private visibleInternal: boolean;
  enabled: boolean;
  toolbar: Toolbar|null;
  protected title?: string;

  constructor(element: Element) {
    super();
    this.element = (element as HTMLElement);
    this.element.classList.add('toolbar-item');
    this.visibleInternal = true;
    this.enabled = true;

    /**
     * Set by the parent toolbar during appending.
     */
    this.toolbar = null;
  }

  setTitle(title: string, actionId: string|undefined = undefined): void {
    if (this.title === title) {
      return;
    }
    this.title = title;
    ARIAUtils.setAccessibleName(this.element, title);
    if (actionId === undefined) {
      Tooltip.install(this.element, title);
    } else {
      Tooltip.installWithActionBinding(this.element, title, actionId);
    }
  }

  setEnabled(value: boolean): void {
    if (this.enabled === value) {
      return;
    }
    this.enabled = value;
    this.applyEnabledState(this.enabled && (!this.toolbar || this.toolbar.enabled));
  }

  applyEnabledState(enabled: boolean): void {
    // @ts-ignore: Ignoring in favor of an `instanceof` check for all the different
    //             kind of HTMLElement classes that have a disabled attribute.
    this.element.disabled = !enabled;
  }

  visible(): boolean {
    return this.visibleInternal;
  }

  setVisible(x: boolean): void {
    if (this.visibleInternal === x) {
      return;
    }
    this.element.classList.toggle('hidden', !x);
    this.visibleInternal = x;
    if (this.toolbar && !(this instanceof ToolbarSeparator)) {
      this.toolbar.hideSeparatorDupes();
    }
  }

  setRightAligned(alignRight: boolean): void {
    this.element.classList.toggle('toolbar-item-right-aligned', alignRight);
  }

  setCompactLayout(_enable: boolean): void {
  }
}

export const enum ToolbarItemWithCompactLayoutEvents {
  CompactLayoutUpdated = 'CompactLayoutUpdated',
}

type ToolbarItemWithCompactLayoutEventTypes = {
  [ToolbarItemWithCompactLayoutEvents.CompactLayoutUpdated]: boolean,
};

export class ToolbarItemWithCompactLayout extends ToolbarItem<ToolbarItemWithCompactLayoutEventTypes> {
  constructor(element: Element) {
    super(element);
  }

  override setCompactLayout(enable: boolean): void {
    this.dispatchEventToListeners(ToolbarItemWithCompactLayoutEvents.CompactLayoutUpdated, enable);
  }
}

export class ToolbarText extends ToolbarItem<void> {
  constructor(text?: string) {
    const element = document.createElement('div');
    element.classList.add('toolbar-text');
    super(element);
    this.element.classList.add('toolbar-text');
    this.setText(text || '');
  }

  text(): string {
    return this.element.textContent || '';
  }

  setText(text: string): void {
    this.element.textContent = text;
  }
}

export class ToolbarButton extends ToolbarItem<ToolbarButton.EventTypes> {
  private readonly glyphElement: Icon;
  private textElement: HTMLElement;
  private text?: string;
  private glyph?: string;
  private icon?: HTMLElement;
  /**
   * TODO(crbug.com/1126026): remove glyph parameter in favor of icon.
   */
  constructor(title: string, glyphOrIcon?: string|HTMLElement, text?: string) {
    const element = document.createElement('button');
    element.classList.add('toolbar-button');
    super(element);
    this.element.addEventListener('click', this.clicked.bind(this), false);
    this.element.addEventListener('mousedown', this.mouseDown.bind(this), false);

    this.glyphElement = Icon.create('', 'toolbar-glyph hidden');
    this.element.appendChild(this.glyphElement);
    this.textElement = this.element.createChild('div', 'toolbar-text hidden');

    this.setTitle(title);
    if (glyphOrIcon) {
      this.setGlyphOrIcon(glyphOrIcon);
    }
    this.setText(text || '');
    this.title = '';
  }

  focus(): void {
    this.element.focus();
  }

  setText(text: string): void {
    if (this.text === text) {
      return;
    }
    this.textElement.textContent = text;
    this.textElement.classList.toggle('hidden', !text);
    this.text = text;
  }

  setGlyphOrIcon(glyphOrIcon: string|HTMLElement): void {
    if (glyphOrIcon instanceof HTMLElement) {
      glyphOrIcon.classList.add('toolbar-icon');
      if (this.icon) {
        this.icon.replaceWith(glyphOrIcon);
      } else {
        this.element.appendChild(glyphOrIcon);
      }
      this.icon = glyphOrIcon;
    } else if (glyphOrIcon) {
      this.setGlyph(glyphOrIcon);
    }
  }

  setGlyph(glyph: string): void {
    if (this.glyph === glyph) {
      return;
    }
    this.glyphElement.setIconType(glyph);
    this.glyphElement.classList.toggle('hidden', !glyph);
    this.element.classList.toggle('toolbar-has-glyph', Boolean(glyph));
    this.glyph = glyph;
  }

  setBackgroundImage(iconURL: string): void {
    this.element.style.backgroundImage = 'url(' + iconURL + ')';
  }

  setSecondary(): void {
    this.element.classList.add('toolbar-button-secondary');
  }

  setDarkText(): void {
    this.element.classList.add('dark-text');
  }

  turnIntoSelect(shrinkable: boolean|undefined = false): void {
    this.element.classList.add('toolbar-has-dropdown');
    if (shrinkable) {
      this.element.classList.add('toolbar-has-dropdown-shrinkable');
    }
    const dropdownArrowIcon = Icon.create('smallicon-triangle-down', 'toolbar-dropdown-arrow');
    this.element.appendChild(dropdownArrowIcon);
  }

  clicked(event: Event): void {
    if (!this.enabled) {
      return;
    }
    this.dispatchEventToListeners(ToolbarButton.Events.Click, event);
    event.consume();
  }

  protected mouseDown(event: MouseEvent): void {
    if (!this.enabled) {
      return;
    }
    this.dispatchEventToListeners(ToolbarButton.Events.MouseDown, event);
  }
}

export namespace ToolbarButton {
  // TODO(crbug.com/1167717): Make this a const enum again
  // eslint-disable-next-line rulesdir/const_enum
  export enum Events {
    Click = 'Click',
    MouseDown = 'MouseDown',
  }

  export type EventTypes = {
    [Events.Click]: Event,
    [Events.MouseDown]: MouseEvent,
  };
}

export class ToolbarInput extends ToolbarItem<ToolbarInput.EventTypes> {
  private prompt: TextPrompt;
  private readonly proxyElement: Element;

  constructor(
      placeholder: string, accessiblePlaceholder?: string, growFactor?: number, shrinkFactor?: number, tooltip?: string,
      completions?: ((arg0: string, arg1: string, arg2?: boolean|undefined) => Promise<Suggestion[]>),
      dynamicCompletions?: boolean) {
    const element = document.createElement('div');
    element.classList.add('toolbar-input');
    super(element);

    const internalPromptElement = this.element.createChild('div', 'toolbar-input-prompt');
    ARIAUtils.setAccessibleName(internalPromptElement, placeholder);
    internalPromptElement.addEventListener('focus', () => this.element.classList.add('focused'));
    internalPromptElement.addEventListener('blur', () => this.element.classList.remove('focused'));

    this.prompt = new TextPrompt();
    this.proxyElement = this.prompt.attach(internalPromptElement);
    this.proxyElement.classList.add('toolbar-prompt-proxy');
    this.proxyElement.addEventListener('keydown', (event: Event) => this.onKeydownCallback(event));
    this.prompt.initialize(completions || ((): Promise<never[]> => Promise.resolve([])), ' ', dynamicCompletions);
    if (tooltip) {
      this.prompt.setTitle(tooltip);
    }
    this.prompt.setPlaceholder(placeholder, accessiblePlaceholder);
    this.prompt.addEventListener(TextPromptEvents.TextChanged, this.onChangeCallback.bind(this));

    if (growFactor) {
      this.element.style.flexGrow = String(growFactor);
    }
    if (shrinkFactor) {
      this.element.style.flexShrink = String(shrinkFactor);
    }

    const clearButton = this.element.createChild('div', 'toolbar-input-clear-button');
    clearButton.title = UIStrings.clearInput;
    clearButton.appendChild(Icon.create('mediumicon-gray-cross-active', 'search-cancel-button'));
    clearButton.addEventListener('click', () => {
      this.setValue('', true);
      this.prompt.focus();
    });

    this.updateEmptyStyles();
  }

  applyEnabledState(enabled: boolean): void {
    this.prompt.setEnabled(enabled);
  }

  setValue(value: string, notify?: boolean): void {
    this.prompt.setText(value);
    if (notify) {
      this.onChangeCallback();
    }
    this.updateEmptyStyles();
  }

  value(): string {
    return this.prompt.textWithCurrentSuggestion();
  }

  private onKeydownCallback(event: Event): void {
    if ((event as KeyboardEvent).key === 'Enter' && this.prompt.text()) {
      this.dispatchEventToListeners(ToolbarInput.Event.EnterPressed, this.prompt.text());
    }
    if (!isEscKey(event) || !this.prompt.text()) {
      return;
    }
    this.setValue('', true);
    event.consume(true);
  }

  private onChangeCallback(): void {
    this.updateEmptyStyles();
    this.dispatchEventToListeners(ToolbarInput.Event.TextChanged, this.prompt.text());
  }

  private updateEmptyStyles(): void {
    this.element.classList.toggle('toolbar-input-empty', !this.prompt.text());
  }
}

export namespace ToolbarInput {
  // TODO(crbug.com/1167717): Make this a const enum again
  // eslint-disable-next-line rulesdir/const_enum
  export enum Event {
    TextChanged = 'TextChanged',
    EnterPressed = 'EnterPressed',
  }

  export interface EventTypes {
    [Event.TextChanged]: string;
    [Event.EnterPressed]: string;
  }
}

export class ToolbarToggle extends ToolbarButton {
  private toggledInternal: boolean;
  private readonly untoggledGlyphOrIcon: string|HTMLElement|undefined;
  private readonly toggledGlyphOrIcon: string|HTMLElement|undefined;

  constructor(title: string, glyphOrIcon?: string|HTMLElement, toggledGlyphOrIcon?: string|HTMLElement) {
    super(title, glyphOrIcon, '');
    this.toggledInternal = false;
    this.untoggledGlyphOrIcon = glyphOrIcon;
    this.toggledGlyphOrIcon = toggledGlyphOrIcon;
    this.element.classList.add('toolbar-state-off');
    ARIAUtils.setPressed(this.element, false);
  }

  toggled(): boolean {
    return this.toggledInternal;
  }

  setToggled(toggled: boolean): void {
    if (this.toggledInternal === toggled) {
      return;
    }
    this.toggledInternal = toggled;
    this.element.classList.toggle('toolbar-state-on', toggled);
    this.element.classList.toggle('toolbar-state-off', !toggled);
    ARIAUtils.setPressed(this.element, toggled);
    if (this.toggledGlyphOrIcon && this.untoggledGlyphOrIcon) {
      this.setGlyphOrIcon(toggled ? this.toggledGlyphOrIcon : this.untoggledGlyphOrIcon);
    }
  }

  setDefaultWithRedColor(withRedColor: boolean): void {
    this.element.classList.toggle('toolbar-default-with-red-color', withRedColor);
  }

  setToggleWithRedColor(toggleWithRedColor: boolean): void {
    this.element.classList.toggle('toolbar-toggle-with-red-color', toggleWithRedColor);
  }

  setToggleWithDot(toggleWithDot: boolean): void {
    this.element.classList.toggle('toolbar-toggle-with-dot', toggleWithDot);
  }
}

export class ToolbarMenuButton extends ToolbarButton {
  private readonly contextMenuHandler: (arg0: ContextMenu) => void;
  private readonly useSoftMenu: boolean;
  private triggerTimeout?: number;
  private lastTriggerTime?: number;
  constructor(contextMenuHandler: (arg0: ContextMenu) => void, useSoftMenu?: boolean) {
    super('', 'largeicon-menu');
    this.contextMenuHandler = contextMenuHandler;
    this.useSoftMenu = Boolean(useSoftMenu);
    ARIAUtils.markAsMenuButton(this.element);
  }

  mouseDown(event: MouseEvent): void {
    if (event.buttons !== 1) {
      super.mouseDown(event);
      return;
    }

    if (!this.triggerTimeout) {
      this.triggerTimeout = window.setTimeout(this.trigger.bind(this, event), 200);
    }
  }

  private trigger(event: Event): void {
    delete this.triggerTimeout;

    // Throttling avoids entering a bad state on Macs when rapidly triggering context menus just
    // after the window gains focus. See crbug.com/655556
    if (this.lastTriggerTime && Date.now() - this.lastTriggerTime < 300) {
      return;
    }
    const contextMenu = new ContextMenu(event, {
      useSoftMenu: this.useSoftMenu,
      x: this.element.totalOffsetLeft(),
      y: this.element.totalOffsetTop() + this.element.offsetHeight,
    });
    this.contextMenuHandler(contextMenu);
    void contextMenu.show();
    this.lastTriggerTime = Date.now();
  }

  clicked(event: Event): void {
    if (this.triggerTimeout) {
      clearTimeout(this.triggerTimeout);
    }
    this.trigger(event);
  }
}

export class ToolbarSettingToggle extends ToolbarToggle {
  private readonly defaultTitle: string;
  private readonly setting: Common.Settings.Setting<boolean>;
  private willAnnounceState: boolean;

  constructor(setting: Common.Settings.Setting<boolean>, glyph: string, title: string) {
    super(title, glyph);
    this.defaultTitle = title;
    this.setting = setting;
    this.settingChanged();
    this.setting.addChangeListener(this.settingChanged, this);

    // Determines whether the toggle state will be announced to a screen reader
    this.willAnnounceState = false;
  }

  private settingChanged(): void {
    const toggled = this.setting.get();
    this.setToggled(toggled);
    const toggleAnnouncement = toggled ? i18nString(UIStrings.pressed) : i18nString(UIStrings.notPressed);
    if (this.willAnnounceState) {
      ARIAUtils.alert(toggleAnnouncement);
    }
    this.willAnnounceState = false;
    this.setTitle(this.defaultTitle);
  }

  clicked(event: Event): void {
    this.willAnnounceState = true;
    this.setting.set(!this.toggled());
    super.clicked(event);
  }
}

export class ToolbarSeparator extends ToolbarItem<void> {
  constructor(spacer?: boolean) {
    const element = document.createElement('div');
    element.classList.add(spacer ? 'toolbar-spacer' : 'toolbar-divider');
    super(element);
  }
}

export interface Provider {
  item(): ToolbarItem|null;
}

export interface ItemsProvider {
  toolbarItems(): ToolbarItem[];
}

export class ToolbarComboBox extends ToolbarItem<void> {
  protected selectElementInternal: HTMLSelectElement;

  constructor(changeHandler: ((arg0: Event) => void)|null, title: string, className?: string) {
    const element = document.createElement('span');
    element.classList.add('toolbar-select-container');
    super(element);
    this.selectElementInternal = (this.element.createChild('select', 'toolbar-item') as HTMLSelectElement);
    const dropdownArrowIcon = Icon.create('smallicon-triangle-down', 'toolbar-dropdown-arrow');
    this.element.appendChild(dropdownArrowIcon);
    if (changeHandler) {
      this.selectElementInternal.addEventListener('change', changeHandler, false);
    }
    ARIAUtils.setAccessibleName(this.selectElementInternal, title);
    super.setTitle(title);
    if (className) {
      this.selectElementInternal.classList.add(className);
    }
  }

  selectElement(): HTMLSelectElement {
    return this.selectElementInternal;
  }

  size(): number {
    return this.selectElementInternal.childElementCount;
  }

  options(): HTMLOptionElement[] {
    return Array.prototype.slice.call(this.selectElementInternal.children, 0);
  }

  addOption(option: Element): void {
    this.selectElementInternal.appendChild(option);
  }

  createOption(label: string, value?: string): Element {
    const option = (this.selectElementInternal.createChild('option') as HTMLOptionElement);
    option.text = label;
    if (typeof value !== 'undefined') {
      option.value = value;
    }
    return option;
  }

  applyEnabledState(enabled: boolean): void {
    super.applyEnabledState(enabled);
    this.selectElementInternal.disabled = !enabled;
  }

  removeOption(option: Element): void {
    this.selectElementInternal.removeChild(option);
  }

  removeOptions(): void {
    this.selectElementInternal.removeChildren();
  }

  selectedOption(): HTMLOptionElement|null {
    if (this.selectElementInternal.selectedIndex >= 0) {
      return this.selectElementInternal[this.selectElementInternal.selectedIndex] as HTMLOptionElement;
    }
    return null;
  }

  select(option: Element): void {
    this.selectElementInternal.selectedIndex =
        // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        Array.prototype.indexOf.call((this.selectElementInternal as any), option);
  }

  setSelectedIndex(index: number): void {
    this.selectElementInternal.selectedIndex = index;
  }

  selectedIndex(): number {
    return this.selectElementInternal.selectedIndex;
  }

  setMaxWidth(width: number): void {
    this.selectElementInternal.style.maxWidth = width + 'px';
  }

  setMinWidth(width: number): void {
    this.selectElementInternal.style.minWidth = width + 'px';
  }
}

export interface Option {
  value: string;
  label: string;
}

export class ToolbarSettingComboBox extends ToolbarComboBox {
  private optionsInternal: Option[];
  private readonly setting: Common.Settings.Setting<string>;
  private muteSettingListener?: boolean;
  constructor(options: Option[], setting: Common.Settings.Setting<string>, accessibleName: string) {
    super(null, accessibleName);
    this.optionsInternal = options;
    this.setting = setting;
    this.selectElementInternal.addEventListener('change', this.valueChanged.bind(this), false);
    this.setOptions(options);
    setting.addChangeListener(this.settingChanged, this);
  }

  setOptions(options: Option[]): void {
    this.optionsInternal = options;
    this.selectElementInternal.removeChildren();
    for (let i = 0; i < options.length; ++i) {
      const dataOption = options[i];
      const option = this.createOption(dataOption.label, dataOption.value);
      this.selectElementInternal.appendChild(option);
      if (this.setting.get() === dataOption.value) {
        this.setSelectedIndex(i);
      }
    }
  }

  value(): string {
    return this.optionsInternal[this.selectedIndex()].value;
  }

  private settingChanged(): void {
    if (this.muteSettingListener) {
      return;
    }

    const value = this.setting.get();
    for (let i = 0; i < this.optionsInternal.length; ++i) {
      if (value === this.optionsInternal[i].value) {
        this.setSelectedIndex(i);
        break;
      }
    }
  }

  private valueChanged(_event: Event): void {
    const option = this.optionsInternal[this.selectedIndex()];
    this.muteSettingListener = true;
    this.setting.set(option.value);
    this.muteSettingListener = false;
  }
}

export class ToolbarCheckbox extends ToolbarItem<void> {
  inputElement: HTMLInputElement;

  constructor(text: string, tooltip?: string, listener?: ((arg0: MouseEvent) => void)) {
    super(CheckboxLabel.create(text));
    this.element.classList.add('checkbox');
    this.inputElement = (this.element as CheckboxLabel).checkboxElement;
    if (tooltip) {
      // install on the checkbox
      Tooltip.install(this.inputElement, tooltip);
      Tooltip.install((this.element as CheckboxLabel).textElement, tooltip);
    }
    if (listener) {
      this.inputElement.addEventListener('click', listener, false);
    }
  }

  checked(): boolean {
    return this.inputElement.checked;
  }

  setChecked(value: boolean): void {
    this.inputElement.checked = value;
  }

  applyEnabledState(enabled: boolean): void {
    super.applyEnabledState(enabled);
    this.inputElement.disabled = !enabled;
  }

  setIndeterminate(indeterminate: boolean): void {
    this.inputElement.indeterminate = indeterminate;
  }
}

export class ToolbarSettingCheckbox extends ToolbarCheckbox {
  constructor(setting: Common.Settings.Setting<boolean>, tooltip?: string, alternateTitle?: string) {
    super(alternateTitle || setting.title() || '', tooltip);
    bindCheckbox(this.inputElement, setting);
  }
}

const registeredToolbarItems: ToolbarItemRegistration[] = [];

export function registerToolbarItem(registration: ToolbarItemRegistration): void {
  registeredToolbarItems.push(registration);
}

function getRegisteredToolbarItems(): ToolbarItemRegistration[] {
  return registeredToolbarItems.filter(
      item => Root.Runtime.Runtime.isDescriptorEnabled({experiment: undefined, condition: item.condition}));
}

export interface ToolbarItemRegistration {
  order?: number;
  location: ToolbarItemLocation;
  separator?: boolean;
  showLabel?: boolean;
  actionId?: string;
  condition?: string;
  loadItem?: (() => Promise<Provider>);
}

// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum ToolbarItemLocation {
  FILES_NAVIGATION_TOOLBAR = 'files-navigator-toolbar',
  MAIN_TOOLBAR_RIGHT = 'main-toolbar-right',
  MAIN_TOOLBAR_LEFT = 'main-toolbar-left',
  STYLES_SIDEBARPANE_TOOLBAR = 'styles-sidebarpane-toolbar',
}
