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

import {
  CodeEditor,
  CodeEditorWrapper,
  IEditorMimeTypeService,
  IEditorServices
} from '@jupyterlab/codeeditor';
import {
  ABCWidgetFactory,
  DocumentRegistry,
  DocumentWidget,
  IDocumentWidget
} from '@jupyterlab/docregistry';
import { textEditorIcon } from '@jupyterlab/ui-components';
import { PromiseDelegate } from '@lumino/coreutils';
import { Message } from '@lumino/messaging';
import { StackedLayout, Widget } from '@lumino/widgets';

/**
 * The data attribute added to a widget that can run code.
 */
const CODE_RUNNER = 'jpCodeRunner';

/**
 * The data attribute added to a widget that can undo.
 */
const UNDOER = 'jpUndoer';

/**
 * A widget for editors.
 */
export class FileEditor extends Widget {
  /**
   * Construct a new editor widget.
   */
  constructor(options: FileEditor.IOptions) {
    super();
    this.addClass('jp-FileEditor');

    const context = (this._context = options.context);
    this._mimeTypeService = options.mimeTypeService;

    const editorWidget = (this._editorWidget = new CodeEditorWrapper({
      factory: options.factory,
      model: context.model,
      editorOptions: {
        config: FileEditor.defaultEditorConfig
      }
    }));
    this._editorWidget.addClass('jp-FileEditorCodeWrapper');
    this._editorWidget.node.dataset[CODE_RUNNER] = 'true';
    this._editorWidget.node.dataset[UNDOER] = 'true';

    this.editor = editorWidget.editor;
    this.model = editorWidget.model;

    void context.ready.then(() => {
      this._onContextReady();
    });

    // Listen for changes to the path.
    this._onPathChanged();
    context.pathChanged.connect(this._onPathChanged, this);

    const layout = (this.layout = new StackedLayout());
    layout.addWidget(editorWidget);
  }

  /**
   * Get the context for the editor widget.
   */
  get context(): DocumentRegistry.Context {
    return this._context;
  }

  /**
   * A promise that resolves when the file editor is ready.
   */
  get ready(): Promise<void> {
    return this._ready.promise;
  }

  /**
   * Handle the DOM events for the widget.
   *
   * @param event - The DOM event sent to the widget.
   *
   * #### Notes
   * This method implements the DOM `EventListener` interface and is
   * called in response to events on the widget's node. It should
   * not be called directly by user code.
   */
  handleEvent(event: Event): void {
    if (!this.model) {
      return;
    }
    switch (event.type) {
      case 'mousedown':
        this._ensureFocus();
        break;
      default:
        break;
    }
  }

  /**
   * Handle `after-attach` messages for the widget.
   */
  protected onAfterAttach(msg: Message): void {
    super.onAfterAttach(msg);
    const node = this.node;
    node.addEventListener('mousedown', this);
  }

  /**
   * Handle `before-detach` messages for the widget.
   */
  protected onBeforeDetach(msg: Message): void {
    const node = this.node;
    node.removeEventListener('mousedown', this);
  }

  /**
   * Handle `'activate-request'` messages.
   */
  protected onActivateRequest(msg: Message): void {
    this._ensureFocus();
  }

  /**
   * Ensure that the widget has focus.
   */
  private _ensureFocus(): void {
    if (!this.editor.hasFocus()) {
      this.editor.focus();
    }
  }

  /**
   * Handle actions that should be taken when the context is ready.
   */
  private _onContextReady(): void {
    if (this.isDisposed) {
      return;
    }

    // Prevent the initial loading from disk from being in the editor history.
    this.editor.clearHistory();
    // Resolve the ready promise.
    this._ready.resolve(undefined);
  }

  /**
   * Handle a change to the path.
   */
  private _onPathChanged(): void {
    const editor = this.editor;
    const localPath = this._context.localPath;

    editor.model.mimeType =
      this._mimeTypeService.getMimeTypeByFilePath(localPath);
  }

  model: CodeEditor.IModel;
  editor: CodeEditor.IEditor;
  private _context: DocumentRegistry.Context;
  private _editorWidget: CodeEditorWrapper;
  private _mimeTypeService: IEditorMimeTypeService;
  private _ready = new PromiseDelegate<void>();
}

/**
 * The namespace for editor widget statics.
 */
export namespace FileEditor {
  /**
   * The options used to create an editor widget.
   */
  export interface IOptions {
    /**
     * A code editor factory.
     */
    factory: CodeEditor.Factory;

    /**
     * The mime type service for the editor.
     */
    mimeTypeService: IEditorMimeTypeService;

    /**
     * The document context associated with the editor.
     */
    context: DocumentRegistry.CodeContext;
  }

  /**
   * File editor default configuration.
   */
  export const defaultEditorConfig: Record<string, any> = {
    lineNumbers: true,
    scrollPastEnd: true
  };
}

/**
 * A document widget for file editor widgets.
 */
export class FileEditorWidget extends DocumentWidget<FileEditor> {
  /**
   * Set URI fragment identifier for text files
   */
  async setFragment(fragment: string): Promise<void> {
    const parsedFragments = fragment.split('=');

    // TODO: expand to allow more schemes of Fragment Identification Syntax
    // reference: https://datatracker.ietf.org/doc/html/rfc5147#section-3
    if (parsedFragments[0] !== '#line') {
      return;
    }

    const positionOrRange = parsedFragments[1];
    let firstLine: string;
    if (positionOrRange.includes(',')) {
      // Only respect range start for now.
      firstLine = positionOrRange.split(',')[0] || '0';
    } else {
      firstLine = positionOrRange;
    }

    // Reveal the line
    return this.context.ready.then(() => {
      const position = {
        line: parseInt(firstLine, 10),
        column: 0
      };
      this.content.editor.setCursorPosition(position);
      this.content.editor.revealPosition(position);
    });
  }
}

/**
 * A widget factory for editors.
 */
export class FileEditorFactory extends ABCWidgetFactory<
  IDocumentWidget<FileEditor>,
  DocumentRegistry.ICodeModel
> {
  /**
   * Construct a new editor widget factory.
   */
  constructor(options: FileEditorFactory.IOptions) {
    super(options.factoryOptions);
    this._services = options.editorServices;
  }

  /**
   * Create a new widget given a context.
   */
  protected createNewWidget(
    context: DocumentRegistry.CodeContext
  ): IDocumentWidget<FileEditor> {
    const func = this._services.factoryService.newDocumentEditor;
    const factory: CodeEditor.Factory = options => {
      // Use same id as document factory
      return func(options);
    };
    const content = new FileEditor({
      factory,
      context,
      mimeTypeService: this._services.mimeTypeService
    });

    content.title.icon = textEditorIcon;
    const widget = new FileEditorWidget({ content, context });
    return widget;
  }

  private _services: IEditorServices;
}

/**
 * The namespace for `FileEditorFactory` class statics.
 */
export namespace FileEditorFactory {
  /**
   * The options used to create an editor widget factory.
   */
  export interface IOptions {
    /**
     * The editor services used by the factory.
     */
    editorServices: IEditorServices;

    /**
     * The factory options associated with the factory.
     */
    factoryOptions: DocumentRegistry.IWidgetFactoryOptions<
      IDocumentWidget<FileEditor>
    >;
  }
}
