
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import type * as Foundation from '../../foundation/foundation.js';

import type {ViewLocationResolver} from './View.js';
import type {Widget} from './Widget.js';

const UIStrings = {
  /**
   * @description Badge label for an entry in the Quick Open menu. Selecting the entry opens the 'Elements' panel.
   */
  elements: 'Elements',
  /**
   * @description Badge label for an entry in the Quick Open menu. Selecting the entry opens the 'Drawer' panel.
   */
  drawer: 'Drawer',
  /**
   * @description Badge label for an entry in the Quick Open menu. Selecting the entry opens the 'Drawer sidebar' panel.
   */
  drawer_sidebar: 'Drawer sidebar',
  /**
   * @description Badge label for an entry in the Quick Open menu. Selecting the entry opens the 'Panel'.
   */
  panel: 'Panel',
  /**
   * @description Badge label for an entry in the Quick Open menu. Selecting the entry opens the 'Network' panel.
   */
  network: 'Network',
  /**
   * @description Badge label for an entry in the Quick Open menu. Selecting the entry opens the 'Settings' panel.
   */
  settings: 'Settings',
  /**
   * @description Badge label for an entry in the Quick Open menu. Selecting the entry opens the 'Sources' panel.
   */
  sources: 'Sources',
} as const;
const str_ = i18n.i18n.registerUIStrings('ui/legacy/ViewRegistration.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export const enum ViewPersistence {
  CLOSEABLE = 'closeable',
  PERMANENT = 'permanent',
  TRANSIENT = 'transient',
}

export const enum ViewLocationValues {
  PANEL = 'panel',
  SETTINGS_VIEW = 'settings-view',
  ELEMENTS_SIDEBAR = 'elements-sidebar',
  SOURCES_SIDEBAR_BOTTOM = 'sources.sidebar-bottom',
  NAVIGATOR_VIEW = 'navigator-view',
  DRAWER_VIEW = 'drawer-view',
  DRAWER_SIDEBAR = 'drawer-sidebar',
  NETWORK_SIDEBAR = 'network-sidebar',
  SOURCES_SIDEBAR_TOP = 'sources.sidebar-top',
  SOURCES_SIDEBAR_TABS = 'sources.sidebar-tabs',
}

export interface ViewRegistration {
  /**
   * The name of the experiment a view is associated with. Enabling and disabling the declared
   * experiment will enable and disable the view respectively.
   */
  experiment?: Root.ExperimentNames.ExperimentName;
  /**
   * A condition is a function that will make the view available if it
   * returns true, and not available, otherwise. Make sure that objects you
   * access from inside the condition function are ready at the time when the
   * setting conditions are checked.
   */
  condition?: Root.Runtime.Condition;
  /**
   * The command added to the command menu used to show the view. It usually follows the shape Show <title> as it must
   * not be localized at declaration since it is localized internally when appending the commands to the command menu.
   * The existing duplication of the declaration of the title is expected to be removed once the migration to the version
   * 2 of the localization model has been completed (crbug.com/1136655).
   */
  commandPrompt: () => Platform.UIString.LocalizedString;

  /**
   * A UI string used as the title of the view.
   */
  title: () => Platform.UIString.LocalizedString;

  /**
   * Whether the view is permanently visible or can be opened temporarily.
   */
  persistence?: ViewPersistence;

  /**
   * Whether the view is a preview feature (a corresponding icon is shown then).
   */
  isPreviewFeature?: boolean;
  /**
   * Unique identifier of the view.
   */
  id: Lowercase<string>;
  /**
   * An identifier for the location of the view. The location is resolved by
   * an extension of type '@UI.ViewLocationResolver'.
   */
  location?: ViewLocationValues;
  /**
   * Whether the view has a toolbar.
   */
  hasToolbar?: boolean;
  /**
   * Returns an instance of the class that wraps the view.
   * The common pattern for implementing this function is loading the module with the wrapping 'Widget'
   * lazily loaded.
   * The DevTools universe is passed along, allowing `loadView` to retrieve necessary dependencies.
   * Prefer passing individual dependencies one by one instead of forwarding the full universe. This
   * makes testing easier.
   * As an example:
   *
   * ```js
   * let loadedElementsModule;
   *
   * async function loadElementsModule() {
   *
   *   if (!loadedElementsModule) {
   *     loadedElementsModule = await import('./elements.js');
   *   }
   *   return loadedElementsModule;
   * }
   * UI.ViewManager.registerViewExtension({
   *   <...>
   *   async loadView(universe) {
   *      const Elements = await loadElementsModule();
   *      const pageResourceLoader = universe.context.get(SDK.PageResourceLoader.PageResourceLoader);
   *      return new Elements.ElementsPanel.ElementsPanel(pageResourceLoader);
   *   },
   *   <...>
   * });
   * ```
   */
  loadView: (universe: Foundation.Universe.Universe) => Promise<Widget>;
  /**
   * Used to sort the views that appear in a shared location.
   */
  order?: number;
  /**
   * The names of the settings the registered view performs as UI for.
   */
  settings?: string[];
  /**
   * Words used to find the view in the Command Menu.
   */
  tags?: Array<() => Platform.UIString.LocalizedString>;
  /**
   * Icon to be used next to view's title.
   */
  iconName?: string;
  /**
   * Whether a view needs to be promoted. A new badge is shown next to the menu items then.
   */
  featurePromotionId?: string;
}

const registeredViewExtensions = new Map<string, ViewRegistration>();

export function registerViewExtension(registration: ViewRegistration): void {
  const viewId = registration.id;
  if (registeredViewExtensions.has(viewId)) {
    throw new Error(`Duplicate view id '${viewId}'`);
  }
  registeredViewExtensions.set(viewId, registration);
}

export function getRegisteredViewExtensions(): ViewRegistration[] {
  return registeredViewExtensions.values()
      .filter(
          view => Root.Runtime.Runtime.isDescriptorEnabled({experiment: view.experiment, condition: view.condition}))
      .toArray();
}

export function maybeRemoveViewExtension(viewId: string): boolean {
  return registeredViewExtensions.delete(viewId);
}

const registeredLocationResolvers: LocationResolverRegistration[] = [];

const viewLocationNameSet = new Set<ViewLocationValues>();

export function registerLocationResolver(registration: LocationResolverRegistration): void {
  const locationName = registration.name;
  if (viewLocationNameSet.has(locationName)) {
    throw new Error(`Duplicate view location name registration '${locationName}'`);
  }
  viewLocationNameSet.add(locationName);
  registeredLocationResolvers.push(registration);
}

export function getRegisteredLocationResolvers(): LocationResolverRegistration[] {
  return registeredLocationResolvers;
}

export function resetViewRegistration(): void {
  registeredViewExtensions.clear();
  registeredLocationResolvers.length = 0;
  viewLocationNameSet.clear();
}

export const enum ViewLocationCategory {
  NONE = '',  // `NONE` must be a falsy value. Legacy code uses if-checks for the category.
  ELEMENTS = 'ELEMENTS',
  DRAWER = 'DRAWER',
  DRAWER_SIDEBAR = 'DRAWER_SIDEBAR',
  PANEL = 'PANEL',
  NETWORK = 'NETWORK',
  SETTINGS = 'SETTINGS',
  SOURCES = 'SOURCES',
}

export function getLocalizedViewLocationCategory(category: ViewLocationCategory): Platform.UIString.LocalizedString {
  switch (category) {
    case ViewLocationCategory.ELEMENTS:
      return i18nString(UIStrings.elements);
    case ViewLocationCategory.DRAWER:
      return i18nString(UIStrings.drawer);
    case ViewLocationCategory.DRAWER_SIDEBAR:
      return i18nString(UIStrings.drawer_sidebar);
    case ViewLocationCategory.PANEL:
      return i18nString(UIStrings.panel);
    case ViewLocationCategory.NETWORK:
      return i18nString(UIStrings.network);
    case ViewLocationCategory.SETTINGS:
      return i18nString(UIStrings.settings);
    case ViewLocationCategory.SOURCES:
      return i18nString(UIStrings.sources);
    case ViewLocationCategory.NONE:
      return i18n.i18n.lockedString('');
  }
}

export interface LocationResolverRegistration {
  name: ViewLocationValues;
  category: ViewLocationCategory;
  loadResolver: () => Promise<ViewLocationResolver>;
}
