// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { ISessionContext, ToolbarRegistry } from '@jupyterlab/apputils';
import { CodeEditor } from '@jupyterlab/codeeditor';
import {
  IChangedArgs as IChangedArgsGeneric,
  PathExt
} from '@jupyterlab/coreutils';
import { IObservableList } from '@jupyterlab/observables';
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
import { Contents, Kernel } from '@jupyterlab/services';
import { ISharedDocument, ISharedFile } from '@jupyter/ydoc';
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
import {
  fileIcon,
  folderIcon,
  imageIcon,
  jsonIcon,
  juliaIcon,
  LabIcon,
  markdownIcon,
  notebookIcon,
  pdfIcon,
  pythonIcon,
  rKernelIcon,
  spreadsheetIcon,
  Toolbar,
  yamlIcon
} from '@jupyterlab/ui-components';
import { ArrayExt, find } from '@lumino/algorithm';
import { PartialJSONValue, ReadonlyPartialJSONValue } from '@lumino/coreutils';
import { DisposableDelegate, IDisposable } from '@lumino/disposable';
import { ISignal, Signal } from '@lumino/signaling';
import { DockLayout, Widget } from '@lumino/widgets';
import { TextModelFactory } from './default';

/**
 * The document registry.
 */
export class DocumentRegistry implements IDisposable {
  /**
   * Construct a new document registry.
   */
  constructor(options: DocumentRegistry.IOptions = {}) {
    const factory = options.textModelFactory;
    this.translator = options.translator || nullTranslator;

    if (factory && factory.name !== 'text') {
      throw new Error('Text model factory must have the name `text`');
    }
    this._modelFactories['text'] = factory || new TextModelFactory(true);

    const fts =
      options.initialFileTypes ||
      DocumentRegistry.getDefaultFileTypes(this.translator);
    fts.forEach(ft => {
      const value: DocumentRegistry.IFileType = {
        ...DocumentRegistry.getFileTypeDefaults(this.translator),
        ...ft
      };
      this._fileTypes.push(value);
    });
  }

  /**
   * A signal emitted when the registry has changed.
   */
  get changed(): ISignal<this, DocumentRegistry.IChangedArgs> {
    return this._changed;
  }

  /**
   * Get whether the document registry has been disposed.
   */
  get isDisposed(): boolean {
    return this._isDisposed;
  }

  /**
   * Dispose of the resources held by the document registry.
   */
  dispose(): void {
    if (this.isDisposed) {
      return;
    }
    this._isDisposed = true;
    for (const modelName in this._modelFactories) {
      this._modelFactories[modelName].dispose();
    }
    for (const widgetName in this._widgetFactories) {
      this._widgetFactories[widgetName].dispose();
    }
    for (const widgetName in this._extenders) {
      this._extenders[widgetName].length = 0;
    }

    this._fileTypes.length = 0;
    Signal.clearData(this);
  }

  /**
   * Add a widget factory to the registry.
   *
   * @param factory - The factory instance to register.
   *
   * @returns A disposable which will unregister the factory.
   *
   * #### Notes
   * If a factory with the given `'name'` is already registered,
   * a warning will be logged, and this will be a no-op.
   * If `'*'` is given as a default extension, the factory will be registered
   * as the global default.
   * If an extension or global default is already registered, this factory
   * will override the existing default.
   * The factory cannot be named an empty string or the string `'default'`.
   */
  addWidgetFactory(factory: DocumentRegistry.WidgetFactory): IDisposable {
    const name = factory.name.toLowerCase();
    if (!name || name === 'default') {
      throw Error('Invalid factory name');
    }
    if (this._widgetFactories[name]) {
      console.warn(`Duplicate registered factory ${name}`);
      return new DisposableDelegate(Private.noOp);
    }
    this._widgetFactories[name] = factory;
    for (const ft of factory.defaultFor || []) {
      if (factory.fileTypes.indexOf(ft) === -1) {
        continue;
      }
      if (ft === '*') {
        this._defaultWidgetFactory = name;
      } else {
        this._defaultWidgetFactories[ft] = name;
      }
    }
    for (const ft of factory.defaultRendered || []) {
      if (factory.fileTypes.indexOf(ft) === -1) {
        continue;
      }
      this._defaultRenderedWidgetFactories[ft] = name;
    }
    // For convenience, store a mapping of file type name -> name
    for (const ft of factory.fileTypes) {
      if (!this._widgetFactoriesForFileType[ft]) {
        this._widgetFactoriesForFileType[ft] = [];
      }
      this._widgetFactoriesForFileType[ft].push(name);
    }
    this._changed.emit({
      type: 'widgetFactory',
      name,
      change: 'added'
    });
    return new DisposableDelegate(() => {
      delete this._widgetFactories[name];
      if (this._defaultWidgetFactory === name) {
        this._defaultWidgetFactory = '';
      }
      for (const ext of Object.keys(this._defaultWidgetFactories)) {
        if (this._defaultWidgetFactories[ext] === name) {
          delete this._defaultWidgetFactories[ext];
        }
      }
      for (const ext of Object.keys(this._defaultRenderedWidgetFactories)) {
        if (this._defaultRenderedWidgetFactories[ext] === name) {
          delete this._defaultRenderedWidgetFactories[ext];
        }
      }
      for (const ext of Object.keys(this._widgetFactoriesForFileType)) {
        ArrayExt.removeFirstOf(this._widgetFactoriesForFileType[ext], name);
        if (this._widgetFactoriesForFileType[ext].length === 0) {
          delete this._widgetFactoriesForFileType[ext];
        }
      }
      for (const ext of Object.keys(this._defaultWidgetFactoryOverrides)) {
        if (this._defaultWidgetFactoryOverrides[ext] === name) {
          delete this._defaultWidgetFactoryOverrides[ext];
        }
      }
      this._changed.emit({
        type: 'widgetFactory',
        name,
        change: 'removed'
      });
    });
  }

  /**
   * Add a model factory to the registry.
   *
   * @param factory - The factory instance.
   *
   * @returns A disposable which will unregister the factory.
   *
   * #### Notes
   * If a factory with the given `name` is already registered, or
   * the given factory is already registered, a warning will be logged
   * and this will be a no-op.
   */
  addModelFactory(factory: DocumentRegistry.ModelFactory): IDisposable {
    const name = factory.name.toLowerCase();
    if (this._modelFactories[name]) {
      console.warn(`Duplicate registered factory ${name}`);
      return new DisposableDelegate(Private.noOp);
    }
    this._modelFactories[name] = factory;
    this._changed.emit({
      type: 'modelFactory',
      name,
      change: 'added'
    });
    return new DisposableDelegate(() => {
      delete this._modelFactories[name];
      this._changed.emit({
        type: 'modelFactory',
        name,
        change: 'removed'
      });
    });
  }

  /**
   * Add a widget extension to the registry.
   *
   * @param widgetName - The name of the widget factory.
   *
   * @param extension - A widget extension.
   *
   * @returns A disposable which will unregister the extension.
   *
   * #### Notes
   * If the extension is already registered for the given
   * widget name, a warning will be logged and this will be a no-op.
   */
  addWidgetExtension(
    widgetName: string,
    extension: DocumentRegistry.WidgetExtension
  ): IDisposable {
    widgetName = widgetName.toLowerCase();
    if (!(widgetName in this._extenders)) {
      this._extenders[widgetName] = [];
    }
    const extenders = this._extenders[widgetName];
    const index = ArrayExt.firstIndexOf(extenders, extension);
    if (index !== -1) {
      console.warn(`Duplicate registered extension for ${widgetName}`);
      return new DisposableDelegate(Private.noOp);
    }
    this._extenders[widgetName].push(extension);
    this._changed.emit({
      type: 'widgetExtension',
      name: widgetName,
      change: 'added'
    });
    return new DisposableDelegate(() => {
      ArrayExt.removeFirstOf(this._extenders[widgetName], extension);
      this._changed.emit({
        type: 'widgetExtension',
        name: widgetName,
        change: 'removed'
      });
    });
  }

  /**
   * Add a file type to the document registry.
   *
   * @param fileType - The file type object to register.
   * @param factories - Optional factories to use for the file type.
   *
   * @returns A disposable which will unregister the command.
   *
   * #### Notes
   * These are used to populate the "Create New" dialog.
   *
   * If no default factory exists for the file type, the first factory will
   * be defined as default factory.
   */
  addFileType(
    fileType: Partial<DocumentRegistry.IFileType>,
    factories?: string[]
  ): IDisposable {
    const value: DocumentRegistry.IFileType = {
      ...DocumentRegistry.getFileTypeDefaults(this.translator),
      ...fileType,
      // fall back to fileIcon if needed
      ...(!(fileType.icon || fileType.iconClass) && { icon: fileIcon })
    };
    this._fileTypes.push(value);

    // Add the filetype to the factory - filetype mapping
    //  We do not change the factory itself
    if (factories) {
      const fileTypeName = value.name.toLowerCase();
      factories
        .map(factory => factory.toLowerCase())
        .forEach(factory => {
          if (!this._widgetFactoriesForFileType[fileTypeName]) {
            this._widgetFactoriesForFileType[fileTypeName] = [];
          }
          if (
            !this._widgetFactoriesForFileType[fileTypeName].includes(factory)
          ) {
            this._widgetFactoriesForFileType[fileTypeName].push(factory);
          }
        });
      if (!this._defaultWidgetFactories[fileTypeName]) {
        this._defaultWidgetFactories[fileTypeName] =
          this._widgetFactoriesForFileType[fileTypeName][0];
      }
    }

    this._changed.emit({
      type: 'fileType',
      name: value.name,
      change: 'added'
    });
    return new DisposableDelegate(() => {
      ArrayExt.removeFirstOf(this._fileTypes, value);
      if (factories) {
        const fileTypeName = value.name.toLowerCase();
        for (const name of factories.map(factory => factory.toLowerCase())) {
          ArrayExt.removeFirstOf(
            this._widgetFactoriesForFileType[fileTypeName],
            name
          );
        }
        if (
          this._defaultWidgetFactories[fileTypeName] ===
          factories[0].toLowerCase()
        ) {
          delete this._defaultWidgetFactories[fileTypeName];
        }
      }
      this._changed.emit({
        type: 'fileType',
        name: fileType.name,
        change: 'removed'
      });
    });
  }

  /**
   * Get a list of the preferred widget factories.
   *
   * @param path - The file path to filter the results.
   *
   * @returns A new array of widget factories.
   *
   * #### Notes
   * Only the widget factories whose associated model factory have
   * been registered will be returned.
   * The first item is considered the default. The returned array
   * has widget factories in the following order:
   * - path-specific default factory
   * - path-specific default rendered factory
   * - global default factory
   * - all other path-specific factories
   * - all other global factories
   */
  preferredWidgetFactories(path: string): DocumentRegistry.WidgetFactory[] {
    const factories = new Set<string>();

    // Get the ordered matching file types.
    const fts = this.getFileTypesForPath(PathExt.basename(path));

    // Start with any user overrides for the defaults.
    fts.forEach(ft => {
      if (ft.name in this._defaultWidgetFactoryOverrides) {
        factories.add(this._defaultWidgetFactoryOverrides[ft.name]);
      }
    });

    // Next add the file type default factories.
    fts.forEach(ft => {
      if (ft.name in this._defaultWidgetFactories) {
        factories.add(this._defaultWidgetFactories[ft.name]);
      }
    });

    // Add the file type default rendered factories.
    fts.forEach(ft => {
      if (ft.name in this._defaultRenderedWidgetFactories) {
        factories.add(this._defaultRenderedWidgetFactories[ft.name]);
      }
    });

    // Add the global default factory.
    if (this._defaultWidgetFactory) {
      factories.add(this._defaultWidgetFactory);
    }

    // Add the file type factories in registration order.
    for (const ft of fts) {
      if (ft.name in this._widgetFactoriesForFileType) {
        for (const n of this._widgetFactoriesForFileType[ft.name]) {
          factories.add(n);
        }
      }
    }

    // Add the rest of the global factories, in registration order.
    if ('*' in this._widgetFactoriesForFileType) {
      for (const n of this._widgetFactoriesForFileType['*']) {
        factories.add(n);
      }
    }

    // Construct the return list, checking to make sure the corresponding
    // model factories are registered.
    const factoryList: DocumentRegistry.WidgetFactory[] = [];
    for (const name of factories) {
      const factory = this._widgetFactories[name];
      if (!factory) {
        continue;
      }
      const modelName = factory.modelName || 'text';
      if (modelName in this._modelFactories) {
        factoryList.push(factory);
      }
    }

    return factoryList;
  }

  /**
   * Get the default rendered widget factory for a path.
   *
   * @param path - The path to for which to find a widget factory.
   *
   * @returns The default rendered widget factory for the path.
   *
   * ### Notes
   * If the widget factory has registered a separate set of `defaultRendered`
   * file types and there is a match in that set, this returns that.
   * Otherwise, this returns the same widget factory as
   * [[defaultWidgetFactory]].
   *
   * The user setting `defaultViewers` took precedence on this one too.
   */
  defaultRenderedWidgetFactory(path: string): DocumentRegistry.WidgetFactory {
    // Get the matching file types.
    const ftNames = this.getFileTypesForPath(PathExt.basename(path)).map(
      ft => ft.name
    );

    // Start with any user overrides for the defaults.
    for (const name in ftNames) {
      if (name in this._defaultWidgetFactoryOverrides) {
        return this._widgetFactories[this._defaultWidgetFactoryOverrides[name]];
      }
    }

    // Find if a there is a default rendered factory for this type.
    for (const name in ftNames) {
      if (name in this._defaultRenderedWidgetFactories) {
        return this._widgetFactories[
          this._defaultRenderedWidgetFactories[name]
        ];
      }
    }

    // Fallback to the default widget factory
    return this.defaultWidgetFactory(path);
  }

  /**
   * Get the default widget factory for a path.
   *
   * @param path - An optional file path to filter the results.
   *
   * @returns The default widget factory for an path.
   *
   * #### Notes
   * This is equivalent to the first value in [[preferredWidgetFactories]].
   */
  defaultWidgetFactory(path?: string): DocumentRegistry.WidgetFactory {
    if (!path) {
      return this._widgetFactories[this._defaultWidgetFactory];
    }
    return this.preferredWidgetFactories(path)[0];
  }

  /**
   * Set overrides for the default widget factory for a file type.
   *
   * Normally, a widget factory informs the document registry which file types
   * it should be the default for using the `defaultFor` option in the
   * IWidgetFactoryOptions. This function can be used to override that after
   * the fact.
   *
   * @param fileType The name of the file type.
   *
   * @param factory The name of the factory.
   *
   * #### Notes
   * If `factory` is undefined, then any override will be unset, and the
   * default factory will revert to the original value.
   *
   * If `factory` or `fileType` are not known to the docregistry, or
   * if `factory` cannot open files of type `fileType`, this will throw
   * an error.
   */
  setDefaultWidgetFactory(fileType: string, factory: string | undefined): void {
    fileType = fileType.toLowerCase();
    if (!this.getFileType(fileType)) {
      throw Error(`Cannot find file type ${fileType}`);
    }
    if (!factory) {
      if (this._defaultWidgetFactoryOverrides[fileType]) {
        delete this._defaultWidgetFactoryOverrides[fileType];
      }
      return;
    }
    if (!this.getWidgetFactory(factory)) {
      throw Error(`Cannot find widget factory ${factory}`);
    }
    factory = factory.toLowerCase();
    const factories = this._widgetFactoriesForFileType[fileType];
    if (
      factory !== this._defaultWidgetFactory &&
      !(factories && factories.includes(factory))
    ) {
      throw Error(`Factory ${factory} cannot view file type ${fileType}`);
    }
    this._defaultWidgetFactoryOverrides[fileType] = factory;
  }

  /**
   * Create an iterator over the widget factories that have been registered.
   *
   * @returns A new iterator of widget factories.
   */
  *widgetFactories(): IterableIterator<DocumentRegistry.WidgetFactory> {
    for (const name in this._widgetFactories) {
      yield this._widgetFactories[name];
    }
  }

  /**
   * Create an iterator over the model factories that have been registered.
   *
   * @returns A new iterator of model factories.
   */
  *modelFactories(): IterableIterator<DocumentRegistry.ModelFactory> {
    for (const name in this._modelFactories) {
      yield this._modelFactories[name];
    }
  }

  /**
   * Create an iterator over the registered extensions for a given widget.
   *
   * @param widgetName - The name of the widget factory.
   *
   * @returns A new iterator over the widget extensions.
   */
  *widgetExtensions(
    widgetName: string
  ): IterableIterator<DocumentRegistry.WidgetExtension> {
    widgetName = widgetName.toLowerCase();
    if (widgetName in this._extenders) {
      for (const extension of this._extenders[widgetName]) {
        yield extension;
      }
    }
  }

  /**
   * Create an iterator over the file types that have been registered.
   *
   * @returns A new iterator of file types.
   */
  *fileTypes(): IterableIterator<DocumentRegistry.IFileType> {
    for (const type of this._fileTypes) {
      yield type;
    }
  }

  /**
   * Get a widget factory by name.
   *
   * @param widgetName - The name of the widget factory.
   *
   * @returns A widget factory instance.
   */
  getWidgetFactory(
    widgetName: string
  ): DocumentRegistry.WidgetFactory | undefined {
    return this._widgetFactories[widgetName.toLowerCase()];
  }

  /**
   * Get a model factory by name.
   *
   * @param name - The name of the model factory.
   *
   * @returns A model factory instance.
   */
  getModelFactory(name: string): DocumentRegistry.ModelFactory | undefined {
    return this._modelFactories[name.toLowerCase()];
  }

  /**
   * Get a file type by name.
   */
  getFileType(name: string): DocumentRegistry.IFileType | undefined {
    name = name.toLowerCase();
    return find(this._fileTypes, fileType => {
      return fileType.name.toLowerCase() === name;
    });
  }

  /**
   * Get a kernel preference.
   *
   * @param path - The file path.
   *
   * @param widgetName - The name of the widget factory.
   *
   * @param kernel - An optional existing kernel model.
   *
   * @returns A kernel preference.
   */
  getKernelPreference(
    path: string,
    widgetName: string,
    kernel?: Partial<Kernel.IModel>
  ): ISessionContext.IKernelPreference | undefined {
    widgetName = widgetName.toLowerCase();
    const widgetFactory = this._widgetFactories[widgetName];
    if (!widgetFactory) {
      return void 0;
    }
    const modelFactory = this.getModelFactory(
      widgetFactory.modelName || 'text'
    );
    if (!modelFactory) {
      return void 0;
    }
    const language = modelFactory.preferredLanguage(PathExt.basename(path));
    const name = kernel && kernel.name;
    const id = kernel && kernel.id;
    return {
      id,
      name,
      language,
      shouldStart: widgetFactory.preferKernel,
      canStart: widgetFactory.canStartKernel,
      shutdownOnDispose: widgetFactory.shutdownOnClose,
      autoStartDefault: widgetFactory.autoStartDefault
    };
  }

  /**
   * Get the best file type given a contents model.
   *
   * @param model - The contents model of interest.
   *
   * @returns The best matching file type.
   */
  getFileTypeForModel(
    model: Partial<Contents.IModel>
  ): DocumentRegistry.IFileType {
    let ft: DocumentRegistry.IFileType | null = null;
    if (model.name || model.path) {
      const name = model.name || PathExt.basename(model.path!);
      const fts = this.getFileTypesForPath(name);
      if (fts.length > 0) {
        ft = fts[0];
      }
    }
    switch (model.type) {
      case 'directory':
        if (ft !== null && ft.contentType === 'directory') {
          return ft;
        }
        return (
          find(this._fileTypes, ft => ft.contentType === 'directory') ||
          DocumentRegistry.getDefaultDirectoryFileType(this.translator)
        );
      case 'notebook':
        if (ft !== null && ft.contentType === 'notebook') {
          return ft;
        }
        return (
          find(this._fileTypes, ft => ft.contentType === 'notebook') ||
          DocumentRegistry.getDefaultNotebookFileType(this.translator)
        );
      default:
        // Find the best matching extension.
        if (ft !== null) {
          return ft;
        }
        return (
          this.getFileType('text') ||
          DocumentRegistry.getDefaultTextFileType(this.translator)
        );
    }
  }

  /**
   * Get the file types that match a file name.
   *
   * @param path - The path of the file.
   *
   * @returns An ordered list of matching file types.
   */
  getFileTypesForPath(path: string): DocumentRegistry.IFileType[] {
    const fts: DocumentRegistry.IFileType[] = [];
    const name = PathExt.basename(path);

    // Look for a pattern match first.
    let ft = find(this._fileTypes, ft => {
      return !!(ft.pattern && name.match(ft.pattern) !== null);
    });
    if (ft) {
      fts.push(ft);
    }

    // Then look by extension name, starting with the longest
    let ext = Private.extname(name);
    while (ext.length > 1) {
      const ftSubset = this._fileTypes.filter(ft =>
        // In Private.extname, the extension is transformed to lower case
        ft.extensions.map(extension => extension.toLowerCase()).includes(ext)
      );
      fts.push(...ftSubset);
      ext = '.' + ext.split('.').slice(2).join('.');
    }
    return fts;
  }

  protected translator: ITranslator;
  private _modelFactories: {
    [key: string]: DocumentRegistry.ModelFactory;
  } = Object.create(null);
  private _widgetFactories: {
    [key: string]: DocumentRegistry.WidgetFactory;
  } = Object.create(null);
  private _defaultWidgetFactory = '';
  private _defaultWidgetFactoryOverrides: {
    [key: string]: string;
  } = Object.create(null);
  private _defaultWidgetFactories: { [key: string]: string } =
    Object.create(null);
  private _defaultRenderedWidgetFactories: {
    [key: string]: string;
  } = Object.create(null);
  private _widgetFactoriesForFileType: {
    [key: string]: string[];
  } = Object.create(null);
  private _fileTypes: DocumentRegistry.IFileType[] = [];
  private _extenders: {
    [key: string]: DocumentRegistry.WidgetExtension[];
  } = Object.create(null);
  private _changed = new Signal<this, DocumentRegistry.IChangedArgs>(this);
  private _isDisposed = false;
}

/**
 * The namespace for the `DocumentRegistry` class statics.
 */
export namespace DocumentRegistry {
  /**
   * The item to be added to document toolbar.
   */
  export interface IToolbarItem extends ToolbarRegistry.IToolbarItem {}

  /**
   * The options used to create a document registry.
   */
  export interface IOptions {
    /**
     * The text model factory for the registry.  A default instance will
     * be used if not given.
     */
    textModelFactory?: ModelFactory;

    /**
     * The initial file types for the registry.
     * The [[DocumentRegistry.defaultFileTypes]] will be used if not given.
     */
    initialFileTypes?: DocumentRegistry.IFileType[];

    /**
     * The application language translator.
     */
    translator?: ITranslator;
  }

  /**
   * The interface for a document model.
   */
  export interface IModel extends IDisposable {
    /**
     * A signal emitted when the document content changes.
     */
    contentChanged: ISignal<this, void>;

    /**
     * A signal emitted when the model state changes.
     */
    stateChanged: ISignal<this, IChangedArgsGeneric<any>>;

    /**
     * The dirty state of the model.
     *
     * #### Notes
     * This should be cleared when the document is loaded from
     * or saved to disk.
     */
    dirty: boolean;

    /**
     * The read-only state of the model.
     */
    readOnly: boolean;

    /**
     * The default kernel name of the document.
     */
    readonly defaultKernelName: string;

    /**
     * The default kernel language of the document.
     */
    readonly defaultKernelLanguage: string;

    /**
     * The shared notebook model.
     */
    readonly sharedModel: ISharedDocument;

    /**
     * Whether this document model supports collaboration when the collaborative
     * flag is enabled globally. Defaults to `false`.
     */
    readonly collaborative?: boolean;

    /**
     * Serialize the model to a string.
     */
    toString(): string;

    /**
     * Deserialize the model from a string.
     *
     * #### Notes
     * Should emit a [contentChanged] signal.
     */
    fromString(value: string): void;

    /**
     * Serialize the model to JSON.
     */
    toJSON(): PartialJSONValue;

    /**
     * Deserialize the model from JSON.
     *
     * #### Notes
     * Should emit a [contentChanged] signal.
     */
    fromJSON(value: ReadonlyPartialJSONValue): void;
  }

  /**
   * The interface for a document model that represents code.
   */
  export interface ICodeModel extends IModel, CodeEditor.IModel {
    sharedModel: ISharedFile;
  }

  /**
   * The document context object.
   */
  export interface IContext<T extends IModel> extends IDisposable {
    /**
     * A signal emitted when the path changes.
     */
    pathChanged: ISignal<this, string>;

    /**
     * A signal emitted when the contentsModel changes.
     */
    fileChanged: ISignal<this, Omit<Contents.IModel, 'content'>>;

    /**
     * A signal emitted on the start and end of a saving operation.
     */
    saveState: ISignal<this, SaveState>;

    /**
     * A signal emitted when the context is disposed.
     */
    disposed: ISignal<this, void>;

    /**
     * Configurable margin used to detect document modification conflicts, in milliseconds
     */
    lastModifiedCheckMargin: number;

    /**
     * Whether the document can be saved via the Contents API.
     */
    canSave?: boolean;

    /**
     * The data model for the document.
     */
    readonly model: T;

    /**
     * The session context object associated with the context.
     */
    readonly sessionContext: ISessionContext;

    /**
     * The current path associated with the document.
     */
    readonly path: string;

    /**
     * The current local path associated with the document.
     * If the document is in the default notebook file browser,
     * this is the same as the path.
     */
    readonly localPath: string;

    /**
     * The document metadata, stored as a services contents model.
     *
     * #### Notes
     * This will be null until the context is 'ready'. Since we only store
     * metadata here, the `content` attribute is removed.
     */
    readonly contentsModel: Omit<Contents.IModel, 'content'> | null;

    /**
     * The url resolver for the context.
     */
    readonly urlResolver: IRenderMime.IResolver;

    /**
     * Whether the context is ready.
     */
    readonly isReady: boolean;

    /**
     * A promise that is fulfilled when the context is ready.
     */
    readonly ready: Promise<void>;

    /**
     * Rename the document.
     */
    rename(newName: string): Promise<void>;

    /**
     * Save the document contents to disk.
     */
    save(): Promise<void>;

    /**
     * Save the document to a different path chosen by the user.
     */
    saveAs(): Promise<void>;

    /**
     * Save the document to a different path chosen by the user.
     */
    download(): Promise<void>;

    /**
     * Revert the document contents to disk contents.
     */
    revert(): Promise<void>;

    /**
     * Create a checkpoint for the file.
     *
     * @returns A promise which resolves with the new checkpoint model when the
     *   checkpoint is created.
     */
    createCheckpoint(): Promise<Contents.ICheckpointModel>;

    /**
     * Delete a checkpoint for the file.
     *
     * @param checkpointID - The id of the checkpoint to delete.
     *
     * @returns A promise which resolves when the checkpoint is deleted.
     */
    deleteCheckpoint(checkpointID: string): Promise<void>;

    /**
     * Restore the file to a known checkpoint state.
     *
     * @param checkpointID - The optional id of the checkpoint to restore,
     *   defaults to the most recent checkpoint.
     *
     * @returns A promise which resolves when the checkpoint is restored.
     */
    restoreCheckpoint(checkpointID?: string): Promise<void>;

    /**
     * List available checkpoints for the file.
     *
     * @returns A promise which resolves with a list of checkpoint models for
     *    the file.
     */
    listCheckpoints(): Promise<Contents.ICheckpointModel[]>;

    /**
     * Add a sibling widget to the document manager.
     *
     * @param widget - The widget to add to the document manager.
     *
     * @param options - The desired options for adding the sibling.
     *
     * @returns A disposable used to remove the sibling if desired.
     *
     * #### Notes
     * It is assumed that the widget has the same model and context
     * as the original widget.
     */
    addSibling(widget: Widget, options?: IOpenOptions): IDisposable;
  }

  /**
   * Document save state
   */
  export type SaveState = 'started' | 'failed' | 'completed';

  /**
   * A type alias for a context.
   */
  export type Context = IContext<IModel>;

  /**
   * A type alias for a code context.
   */
  export type CodeContext = IContext<ICodeModel>;

  /**
   * The options used to initialize a widget factory.
   */
  export interface IWidgetFactoryOptions<T extends Widget = Widget>
    extends Omit<
      IRenderMime.IDocumentWidgetFactoryOptions,
      'primaryFileType' | 'toolbarFactory'
    > {
    /**
     * Whether to automatically start the preferred kernel
     */
    readonly autoStartDefault?: boolean;

    /**
     * Identifier of the content provider required for the widget (if any).
     * @experimental
     */
    readonly contentProviderId?: string;

    /**
     * Whether the widget factory is read only.
     */
    readonly readOnly?: boolean;

    /**
     * Whether the widgets prefer having a kernel started.
     */
    readonly preferKernel?: boolean;

    /**
     * Whether the widgets can start a kernel when opened.
     */
    readonly canStartKernel?: boolean;

    /**
     * Whether the kernel should be shutdown when the widget is closed.
     */
    readonly shutdownOnClose?: boolean;

    /**
     * A function producing toolbar widgets, overriding the default toolbar widgets.
     */
    readonly toolbarFactory?: (
      widget: T
    ) =>
      | DocumentRegistry.IToolbarItem[]
      | IObservableList<DocumentRegistry.IToolbarItem>;
  }

  /**
   * The options used to open a widget.
   */
  export interface IOpenOptions {
    /**
     * The reference widget id for the insert location.
     *
     * The default is `null`.
     */
    ref?: string | null;

    /**
     * The supported insertion modes.
     *
     * An insert mode is used to specify how a widget should be added
     * to the main area relative to a reference widget.
     */
    mode?: DockLayout.InsertMode;

    /**
     * Whether to activate the widget.  Defaults to `true`.
     */
    activate?: boolean;

    /**
     * The rank order of the widget among its siblings.
     *
     * #### Notes
     * This field may be used or ignored depending on shell implementation.
     */
    rank?: number;

    /**
     * Type of widget to open
     *
     * #### Notes
     * This is the key used to load user customization.
     * Its typical value is: a factory name or the widget id (if singleton)
     */
    type?: string;
  }

  /**
   * The interface for a widget factory.
   */
  export interface IWidgetFactory<T extends IDocumentWidget, U extends IModel>
    extends IDisposable,
      IWidgetFactoryOptions {
    /**
     * A signal emitted when a new widget is created.
     */
    widgetCreated: ISignal<IWidgetFactory<T, U>, T>;

    /**
     * Create a new widget given a context.
     *
     * @param source - A widget to clone
     *
     * #### Notes
     * It should emit the [widgetCreated] signal with the new widget.
     */
    createNew(context: IContext<U>, source?: T): T;

    /**
     * Identifier of the content provider required for the widget (if any).
     * @experimental
     */
    contentProviderId?: string;
  }

  /**
   * A type alias for a standard widget factory.
   */
  export type WidgetFactory = IWidgetFactory<IDocumentWidget, IModel>;

  /**
   * An interface for a widget extension.
   */
  export interface IWidgetExtension<T extends Widget, U extends IModel> {
    /**
     * Create a new extension for a given widget.
     */
    createNew(widget: T, context: IContext<U>): IDisposable | void;
  }

  /**
   * A type alias for a standard widget extension.
   */
  export type WidgetExtension = IWidgetExtension<Widget, IModel>;

  /**
   * The interface for a model factory.
   */
  export interface IModelFactory<
    T extends IModel,
    U extends ISharedDocument = ISharedDocument
  > extends IDisposable {
    /**
     * The name of the model.
     */
    readonly name: string;

    /**
     * The content type of the file (defaults to `"file"`).
     */
    readonly contentType: Contents.ContentType;

    /**
     * The format of the file (defaults to `"text"`).
     */
    readonly fileFormat: Contents.FileFormat;

    /**
     * Whether the model is collaborative or not.
     */
    readonly collaborative?: boolean;

    /**
     * Create a new model for a given path.
     *
     * @param options - Optional parameters to construct the model.
     *
     * @returns A new document model.
     */
    createNew(options?: IModelOptions<U>): T;

    /**
     * Get the preferred kernel language given a file path.
     */
    preferredLanguage(path: string): string;
  }

  /**
   * The options used to create a document model.
   */
  export interface IModelOptions<T extends ISharedDocument = ISharedDocument> {
    /**
     * The preferred language.
     */
    languagePreference?: string;
    /**
     * The shared model.
     */
    sharedModel?: T;
    /**
     * Whether the model is collaborative or not.
     */
    collaborationEnabled?: boolean;
  }

  /**
   * A type alias for a standard model factory.
   */
  export type ModelFactory = IModelFactory<IModel>;

  /**
   * A type alias for a code model factory.
   */
  export type CodeModelFactory = IModelFactory<ICodeModel>;

  /**
   * An interface for a file type.
   */
  export interface IFileType extends IRenderMime.IFileType {
    /**
     * The icon for the file type.
     */
    readonly icon?: LabIcon;

    /**
     * The content type of the new file.
     */
    readonly contentType: Contents.ContentType;

    /**
     * The format of the new file.
     */
    readonly fileFormat: Contents.FileFormat;
  }

  /**
   * An arguments object for the `changed` signal.
   */
  export interface IChangedArgs {
    /**
     * The type of the changed item.
     */
    readonly type:
      | 'widgetFactory'
      | 'modelFactory'
      | 'widgetExtension'
      | 'fileType';

    /**
     * The name of the item or the widget factory being extended.
     */
    readonly name?: string;

    /**
     * Whether the item was added or removed.
     */
    readonly change: 'added' | 'removed';
  }

  /**
   * The defaults used for a file type.
   *
   * @param translator - The application language translator.
   *
   * @returns The default file type.
   */
  export function getFileTypeDefaults(translator?: ITranslator): IFileType {
    translator = translator || nullTranslator;
    const trans = translator?.load('jupyterlab');

    return {
      name: 'default',
      displayName: trans.__('default'),
      extensions: [],
      mimeTypes: [],
      contentType: 'file',
      fileFormat: 'text'
    };
  }

  /**
   * The default text file type used by the document registry.
   *
   * @param translator - The application language translator.
   *
   * @returns The default text file type.
   */
  export function getDefaultTextFileType(translator?: ITranslator): IFileType {
    translator = translator || nullTranslator;
    const trans = translator?.load('jupyterlab');
    const fileTypeDefaults = getFileTypeDefaults(translator);

    return {
      ...fileTypeDefaults,
      name: 'text',
      displayName: trans.__('Text'),
      mimeTypes: ['text/plain'],
      extensions: ['.txt'],
      icon: fileIcon
    };
  }

  /**
   * The default notebook file type used by the document registry.
   *
   * @param translator - The application language translator.
   *
   * @returns The default notebook file type.
   */
  export function getDefaultNotebookFileType(
    translator?: ITranslator
  ): IFileType {
    translator = translator || nullTranslator;
    const trans = translator?.load('jupyterlab');

    return {
      ...getFileTypeDefaults(translator),
      name: 'notebook',
      displayName: trans.__('Notebook'),
      mimeTypes: ['application/x-ipynb+json'],
      extensions: ['.ipynb'],
      contentType: 'notebook',
      fileFormat: 'json',
      icon: notebookIcon
    };
  }

  /**
   * The default directory file type used by the document registry.
   *
   * @param translator - The application language translator.
   *
   * @returns The default directory file type.
   */
  export function getDefaultDirectoryFileType(
    translator?: ITranslator
  ): IFileType {
    translator = translator || nullTranslator;
    const trans = translator?.load('jupyterlab');

    return {
      ...getFileTypeDefaults(translator),
      name: 'directory',
      displayName: trans.__('Directory'),
      extensions: [],
      mimeTypes: ['text/directory'],
      contentType: 'directory',
      icon: folderIcon
    };
  }

  /**
   * The default file types used by the document registry.
   *
   * @param translator - The application language translator.
   *
   * @returns The default directory file types.
   */
  export function getDefaultFileTypes(
    translator?: ITranslator
  ): ReadonlyArray<Partial<IFileType>> {
    translator = translator || nullTranslator;
    const trans = translator?.load('jupyterlab');

    return [
      getDefaultTextFileType(translator),
      getDefaultNotebookFileType(translator),
      getDefaultDirectoryFileType(translator),
      {
        name: 'markdown',
        displayName: trans.__('Markdown File'),
        extensions: ['.md'],
        mimeTypes: ['text/markdown'],
        icon: markdownIcon
      },
      {
        name: 'PDF',
        displayName: trans.__('PDF File'),
        extensions: ['.pdf'],
        mimeTypes: ['application/pdf'],
        icon: pdfIcon
      },
      {
        name: 'python',
        displayName: trans.__('Python File'),
        extensions: ['.py'],
        mimeTypes: ['text/x-python'],
        icon: pythonIcon
      },
      {
        name: 'json',
        displayName: trans.__('JSON File'),
        extensions: ['.json'],
        mimeTypes: ['application/json'],
        icon: jsonIcon
      },
      {
        name: 'jsonl',
        displayName: trans.__('JSONLines File'),
        extensions: ['.jsonl', '.ndjson'],
        mimeTypes: [
          'text/jsonl',
          'application/jsonl',
          'application/json-lines'
        ],
        icon: jsonIcon
      },
      {
        name: 'julia',
        displayName: trans.__('Julia File'),
        extensions: ['.jl'],
        mimeTypes: ['text/x-julia'],
        icon: juliaIcon
      },
      {
        name: 'csv',
        displayName: trans.__('CSV File'),
        extensions: ['.csv'],
        mimeTypes: ['text/csv'],
        icon: spreadsheetIcon
      },
      {
        name: 'tsv',
        displayName: trans.__('TSV File'),
        extensions: ['.tsv'],
        mimeTypes: ['text/csv'],
        icon: spreadsheetIcon
      },
      {
        name: 'r',
        displayName: trans.__('R File'),
        mimeTypes: ['text/x-rsrc'],
        extensions: ['.R'],
        icon: rKernelIcon
      },
      {
        name: 'yaml',
        displayName: trans.__('YAML File'),
        mimeTypes: ['text/x-yaml', 'text/yaml'],
        extensions: ['.yaml', '.yml'],
        icon: yamlIcon
      },
      {
        name: 'svg',
        displayName: trans.__('Image'),
        mimeTypes: ['image/svg+xml'],
        extensions: ['.svg'],
        icon: imageIcon,
        fileFormat: 'base64'
      },
      {
        name: 'tiff',
        displayName: trans.__('Image'),
        mimeTypes: ['image/tiff'],
        extensions: ['.tif', '.tiff'],
        icon: imageIcon,
        fileFormat: 'base64'
      },
      {
        name: 'jpeg',
        displayName: trans.__('Image'),
        mimeTypes: ['image/jpeg'],
        extensions: ['.jpg', '.jpeg'],
        icon: imageIcon,
        fileFormat: 'base64'
      },
      {
        name: 'gif',
        displayName: trans.__('Image'),
        mimeTypes: ['image/gif'],
        extensions: ['.gif'],
        icon: imageIcon,
        fileFormat: 'base64'
      },
      {
        name: 'png',
        displayName: trans.__('Image'),
        mimeTypes: ['image/png'],
        extensions: ['.png'],
        icon: imageIcon,
        fileFormat: 'base64'
      },
      {
        name: 'bmp',
        displayName: trans.__('Image'),
        mimeTypes: ['image/bmp'],
        extensions: ['.bmp'],
        icon: imageIcon,
        fileFormat: 'base64'
      },
      {
        name: 'webp',
        displayName: trans.__('Image'),
        mimeTypes: ['image/webp'],
        extensions: ['.webp'],
        icon: imageIcon,
        fileFormat: 'base64'
      }
    ];
  }
}

/**
 * An interface for a document widget.
 */
export interface IDocumentWidget<
  T extends Widget = Widget,
  U extends DocumentRegistry.IModel = DocumentRegistry.IModel
> extends Widget {
  /**
   * The content widget.
   */
  readonly content: T;

  /**
   * The context associated with the document.
   */
  readonly context: DocumentRegistry.IContext<U>;

  /**
   * Whether the document has an auto-generated name or not.
   *
   * #### Notes
   * A document has auto-generated name if its name is untitled and up
   * to the instant the user saves it manually for the first time.
   */
  isUntitled?: boolean;

  /**
   * A promise resolving after the content widget is revealed.
   */
  readonly revealed: Promise<void>;

  /**
   * The toolbar for the widget.
   */
  readonly toolbar: Toolbar<Widget>;

  /**
   * Set URI fragment identifier.
   */
  setFragment(fragment: string): void;
}

/**
 * A private namespace for DocumentRegistry data.
 */
namespace Private {
  /**
   * Get the extension name of a path.
   *
   * @param path - string.
   *
   * #### Notes
   * Dotted filenames (e.g. `".table.json"` are allowed).
   */
  export function extname(path: string): string {
    const parts = PathExt.basename(path).split('.');
    parts.shift();
    const ext = '.' + parts.join('.');
    return ext.toLowerCase();
  }
  /**
   * A no-op function.
   */
  export function noOp(): void {
    /* no-op */
  }
}
