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

import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import type * as TextUtils from '../../models/text_utils/text_utils.js';
import * as IconButton from '../../ui/components/icon_button/icon_button.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import {ConsoleFilter, FilterType, type LevelsMask} from './ConsoleFilter.js';
import consoleSidebarStyles from './consoleSidebar.css.js';
import type {ConsoleViewMessage} from './ConsoleViewMessage.js';

const UIStrings = {
  /**
   * @description Filter name in Console Sidebar of the Console panel. This is shown when we fail to
   * parse a URL when trying to display console messages from each URL separately. This might be
   * because the console message does not come from any particular URL. This should be translated as
   * a term that indicates 'not one of the other URLs listed here'.
   */
  other: '<other>',
  /**
   *@description Text in Console Sidebar of the Console panel to show how many user messages exist.
   */
  dUserMessages: '{n, plural, =0 {No user messages} =1 {# user message} other {# user messages}}',
  /**
   *@description Text in Console Sidebar of the Console panel to show how many messages exist.
   */
  dMessages: '{n, plural, =0 {No messages} =1 {# message} other {# messages}}',
  /**
   *@description Text in Console Sidebar of the Console panel to show how many errors exist.
   */
  dErrors: '{n, plural, =0 {No errors} =1 {# error} other {# errors}}',
  /**
   *@description Text in Console Sidebar of the Console panel to show how many warnings exist.
   */
  dWarnings: '{n, plural, =0 {No warnings} =1 {# warning} other {# warnings}}',
  /**
   *@description Text in Console Sidebar of the Console panel to show how many info messages exist.
   */
  dInfo: '{n, plural, =0 {No info} =1 {# info} other {# info}}',
  /**
   *@description Text in Console Sidebar of the Console panel to show how many verbose messages exist.
   */
  dVerbose: '{n, plural, =0 {No verbose} =1 {# verbose} other {# verbose}}',
};
const str_ = i18n.i18n.registerUIStrings('panels/console/ConsoleSidebar.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export class ConsoleSidebar extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) {
  private readonly tree: UI.TreeOutline.TreeOutlineInShadow;
  private selectedTreeElement: UI.TreeOutline.TreeElement|null;
  private readonly treeElements: FilterTreeElement[];

  constructor() {
    super(true);
    this.setMinimumSize(125, 0);

    this.tree = new UI.TreeOutline.TreeOutlineInShadow(UI.TreeOutline.TreeVariant.NAVIGATION_TREE);
    this.tree.addEventListener(UI.TreeOutline.Events.ElementSelected, this.selectionChanged.bind(this));
    this.tree.registerRequiredCSS(consoleSidebarStyles);
    this.tree.hideOverflow();

    this.contentElement.setAttribute('jslog', `${VisualLogging.pane('sidebar').track({resize: true})}`);
    this.contentElement.appendChild(this.tree.element);
    this.selectedTreeElement = null;
    this.treeElements = [];
    const selectedFilterSetting =
        Common.Settings.Settings.instance().createSetting<string|null>('console.sidebar-selected-filter', null);

    const consoleAPIParsedFilters = [{
      key: FilterType.Source,
      text: Common.Console.FrontendMessageSource.ConsoleAPI,
      negative: false,
      regex: undefined,
    }];
    this.appendGroup(
        GroupName.ALL, [], ConsoleFilter.allLevelsFilterValue(), IconButton.Icon.create('list'), selectedFilterSetting);
    this.appendGroup(
        GroupName.CONSOLE_API, consoleAPIParsedFilters, ConsoleFilter.allLevelsFilterValue(),
        IconButton.Icon.create('profile'), selectedFilterSetting);
    this.appendGroup(
        GroupName.ERROR, [], ConsoleFilter.singleLevelMask(Protocol.Log.LogEntryLevel.Error),
        IconButton.Icon.create('cross-circle'), selectedFilterSetting);
    this.appendGroup(
        GroupName.WARNING, [], ConsoleFilter.singleLevelMask(Protocol.Log.LogEntryLevel.Warning),
        IconButton.Icon.create('warning'), selectedFilterSetting);
    this.appendGroup(
        GroupName.INFO, [], ConsoleFilter.singleLevelMask(Protocol.Log.LogEntryLevel.Info),
        IconButton.Icon.create('info'), selectedFilterSetting);
    this.appendGroup(
        GroupName.VERBOSE, [], ConsoleFilter.singleLevelMask(Protocol.Log.LogEntryLevel.Verbose),
        IconButton.Icon.create('bug'), selectedFilterSetting);
    const selectedTreeElementName = selectedFilterSetting.get();
    const defaultTreeElement =
        this.treeElements.find(x => x.name() === selectedTreeElementName) || this.treeElements[0];
    defaultTreeElement.select();
  }

  private appendGroup(
      name: string, parsedFilters: TextUtils.TextUtils.ParsedFilter[], levelsMask: LevelsMask,
      icon: IconButton.Icon.Icon, selectedFilterSetting: Common.Settings.Setting<string|null>): void {
    const filter = new ConsoleFilter(name, parsedFilters, null, levelsMask);
    const treeElement = new FilterTreeElement(filter, icon, selectedFilterSetting);
    this.tree.appendChild(treeElement);
    this.treeElements.push(treeElement);
  }

  clear(): void {
    for (const treeElement of this.treeElements) {
      treeElement.clear();
    }
  }

  onMessageAdded(viewMessage: ConsoleViewMessage): void {
    for (const treeElement of this.treeElements) {
      treeElement.onMessageAdded(viewMessage);
    }
  }

  shouldBeVisible(viewMessage: ConsoleViewMessage): boolean {
    if (this.selectedTreeElement instanceof ConsoleSidebarTreeElement) {
      return this.selectedTreeElement.filter().shouldBeVisible(viewMessage);
    }
    return true;
  }

  private selectionChanged(event: Common.EventTarget.EventTargetEvent<UI.TreeOutline.TreeElement>): void {
    this.selectedTreeElement = event.data;
    this.dispatchEventToListeners(Events.FILTER_SELECTED);
  }
}

export const enum Events {
  FILTER_SELECTED = 'FilterSelected',
}

export interface EventTypes {
  [Events.FILTER_SELECTED]: void;
}

class ConsoleSidebarTreeElement extends UI.TreeOutline.TreeElement {
  protected filterInternal: ConsoleFilter;

  constructor(title: string|Node, filter: ConsoleFilter) {
    super(title);
    this.filterInternal = filter;
  }

  filter(): ConsoleFilter {
    return this.filterInternal;
  }
}

export class URLGroupTreeElement extends ConsoleSidebarTreeElement {
  private countElement: HTMLElement;
  private messageCount: number;

  constructor(filter: ConsoleFilter) {
    super(filter.name, filter);
    this.countElement = this.listItemElement.createChild('span', 'count');
    const icon = IconButton.Icon.create('document');
    this.setLeadingIcons([icon]);
    this.messageCount = 0;
  }

  incrementAndUpdateCounter(): void {
    this.messageCount++;
    this.countElement.textContent = `${this.messageCount}`;
  }
}

const enum GroupName {
  CONSOLE_API = 'user message',
  ALL = 'message',
  ERROR = 'error',
  WARNING = 'warning',
  INFO = 'info',
  VERBOSE = 'verbose',
}

/**
 * Maps the GroupName for a filter to the UIString used to render messages.
 * Stored here so we only construct it once at runtime, rather than everytime we
 * construct a filter or get a new message.
 */
const stringForFilterSidebarItemMap = new Map<GroupName, string>([
  [GroupName.CONSOLE_API, UIStrings.dUserMessages],
  [GroupName.ALL, UIStrings.dMessages],
  [GroupName.ERROR, UIStrings.dErrors],
  [GroupName.WARNING, UIStrings.dWarnings],
  [GroupName.INFO, UIStrings.dInfo],
  [GroupName.VERBOSE, UIStrings.dVerbose],
]);

export class FilterTreeElement extends ConsoleSidebarTreeElement {
  private readonly selectedFilterSetting: Common.Settings.Setting<string|null>;
  private readonly urlTreeElements: Map<string|null, URLGroupTreeElement>;
  private messageCount: number;
  private uiStringForFilterCount: string;

  constructor(
      filter: ConsoleFilter, icon: IconButton.Icon.Icon, selectedFilterSetting: Common.Settings.Setting<string|null>) {
    super(filter.name, filter);
    this.uiStringForFilterCount = stringForFilterSidebarItemMap.get(filter.name as GroupName) || '';
    this.selectedFilterSetting = selectedFilterSetting;
    this.urlTreeElements = new Map();
    this.setLeadingIcons([icon]);
    this.messageCount = 0;
    this.updateCounter();
  }

  clear(): void {
    this.urlTreeElements.clear();
    this.removeChildren();
    this.messageCount = 0;
    this.updateCounter();
  }

  name(): string {
    return this.filterInternal.name;
  }

  override onselect(selectedByUser?: boolean): boolean {
    this.selectedFilterSetting.set(this.filterInternal.name);
    return super.onselect(selectedByUser);
  }

  private updateCounter(): void {
    this.title = this.updateGroupTitle(this.messageCount);
    this.setExpandable(Boolean(this.childCount()));
  }

  private updateGroupTitle(messageCount: number): string {
    if (this.uiStringForFilterCount) {
      // eslint-disable-next-line rulesdir/l10n-i18nString-call-only-with-uistrings
      return i18nString(this.uiStringForFilterCount, {n: messageCount});
    }
    return '';
  }

  onMessageAdded(viewMessage: ConsoleViewMessage): void {
    const message = viewMessage.consoleMessage();
    const shouldIncrementCounter = message.type !== SDK.ConsoleModel.FrontendMessageType.Command &&
        message.type !== SDK.ConsoleModel.FrontendMessageType.Result && !message.isGroupMessage();
    if (!this.filterInternal.shouldBeVisible(viewMessage) || !shouldIncrementCounter) {
      return;
    }
    const child = this.childElement(message.url);
    child.incrementAndUpdateCounter();
    this.messageCount++;
    this.updateCounter();
  }

  private childElement(url?: Platform.DevToolsPath.UrlString): URLGroupTreeElement {
    const urlValue = url || null;
    let child = this.urlTreeElements.get(urlValue);
    if (child) {
      return child;
    }

    const filter = this.filterInternal.clone();
    const parsedURL = urlValue ? Common.ParsedURL.ParsedURL.fromString(urlValue) : null;
    if (urlValue) {
      filter.name = parsedURL ? parsedURL.displayName : urlValue;
    } else {
      filter.name = i18nString(UIStrings.other);
    }
    filter.parsedFilters.push({key: FilterType.Url, text: urlValue, negative: false, regex: undefined});
    child = new URLGroupTreeElement(filter);
    if (urlValue) {
      child.tooltip = urlValue;
    }
    this.urlTreeElements.set(urlValue, child);
    this.appendChild(child);
    return child;
  }
}
