import { JupyterFrontEnd } from '@jupyterlab/application';
import { CodeEditor } from '@jupyterlab/codeeditor';
import {
  CompletionHandler,
  ICompletionManager,
  CompleterModel
} from '@jupyterlab/completer';
import { IDocumentWidget } from '@jupyterlab/docregistry';
import { NotebookPanel } from '@jupyterlab/notebook';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { ILSPCompletionThemeManager } from '@krassowski/completion-theme/lib/types';
import type * as CodeMirror from 'codemirror';

import { CodeCompletion as LSPCompletionSettings } from '../../_completion';
import { IEditorChangedData, WidgetAdapter } from '../../adapters/adapter';
import { NotebookAdapter } from '../../adapters/notebook/notebook';
import { IDocumentConnectionData } from '../../connection_manager';
import { CodeMirrorIntegration } from '../../editor_integration/codemirror';
import { FeatureSettings, IFeatureLabIntegration } from '../../feature';
import {
  AdditionalCompletionTriggerKinds,
  CompletionTriggerKind,
  ExtendedCompletionTriggerKind
} from '../../lsp';
import { ILSPAdapterManager, ILSPLogConsole } from '../../tokens';

import { LSPConnector } from './completion_handler';
import { LazyCompletionItem } from './item';
import { LSPCompleterModel } from './model';
import { ICompletionData, LSPCompletionRenderer } from './renderer';

const DOC_PANEL_SELECTOR = '.jp-Completer-docpanel';
const DOC_PANEL_PLACEHOLDER_CLASS = 'lsp-completer-placeholder';
const LSP_COMPLETER_CLASS = 'lsp-completer';

export class CompletionCM extends CodeMirrorIntegration {
  private _completionCharacters: string[];

  get settings() {
    return super.settings as FeatureSettings<LSPCompletionSettings>;
  }

  get completionCharacters() {
    if (
      this._completionCharacters == null ||
      !this._completionCharacters.length
    ) {
      this._completionCharacters =
        this.connection.getLanguageCompletionCharacters();
    }
    return this._completionCharacters;
  }

  // public handleCompletion(completions: lsProtocol.CompletionItem[]) {
  // TODO: populate the (already displayed) completions list if the completions timed out initially?
  // }

  afterChange(change: CodeMirror.EditorChange): void {
    // TODO: maybe the completer could be kicked off in the handleChange() method directly; signature help still
    //  requires an up-to-date virtual document on the LSP side, so we need to wait for sync.

    // note: trigger character completion need to be have a higher priority than auto-invoked completion
    // because the latter does not work for on-dot completion due to suppression of trivial suggestions
    // see gh430
    let last_character = this.extract_last_character(change);
    if (this.completionCharacters.indexOf(last_character) > -1) {
      this.virtual_editor.console.log(
        'Will invoke completer after',
        last_character
      );
      (this.feature.labIntegration as CompletionLabIntegration)
        .invoke_completer(CompletionTriggerKind.TriggerCharacter)
        .catch(this.console.warn);
      return;
    }

    if (
      change.text &&
      change.text[0].length == 1 &&
      this.settings.composite.continuousHinting
    ) {
      (this.feature.labIntegration as CompletionLabIntegration)
        .invoke_completer(AdditionalCompletionTriggerKinds.AutoInvoked)
        .catch(this.console.warn);
    }
  }
}

export class CompletionLabIntegration implements IFeatureLabIntegration {
  // TODO: maybe instead of creating it each time, keep a hash map instead?
  protected current_completion_connector: LSPConnector;
  protected current_completion_handler: CompletionHandler;
  protected current_adapter: WidgetAdapter<IDocumentWidget> | null = null;
  protected renderer: LSPCompletionRenderer;
  private _latestActiveItem: LazyCompletionItem | null = null;
  private _signalConnected: boolean = false;
  private _disabled: boolean;

  constructor(
    private app: JupyterFrontEnd,
    private completionManager: ICompletionManager,
    public settings: FeatureSettings<LSPCompletionSettings>,
    private adapterManager: ILSPAdapterManager,
    private completionThemeManager: ILSPCompletionThemeManager,
    private console: ILSPLogConsole,
    private renderMimeRegistry: IRenderMimeRegistry
  ) {
    const markdown_renderer =
      this.renderMimeRegistry.createRenderer('text/markdown');
    this.renderer = new LSPCompletionRenderer({
      integrator: this,
      markdownRenderer: markdown_renderer,
      latexTypesetter: this.renderMimeRegistry.latexTypesetter,
      console: console.scope('renderer')
    });
    this.renderer.activeChanged.connect(this.active_completion_changed, this);
    this.renderer.itemShown.connect(this.resolve_and_update, this);
    this._signalConnected = false;
    this._disabled = false;
    // TODO: figure out a better way to disable lab integration elements (postpone initialization?)
    settings.ready
      .then(() => {
        this._disabled = settings.composite.disable;
        if (!settings.composite.disable) {
          adapterManager.adapterChanged.connect(this.swap_adapter, this);
          this._signalConnected = true;
        }
      })
      .catch(console.warn);

    settings.changed.connect(() => {
      this._disabled = settings.composite.disable;
      if (!settings.composite.disable) {
        document.body.dataset.lspCompleterLayout =
          this.settings.composite.layout;
        completionThemeManager.set_theme(this.settings.composite.theme);
        completionThemeManager.set_icons_overrides(
          this.settings.composite.typesMap
        );
        if (!this._signalConnected) {
          adapterManager.adapterChanged.connect(this.swap_adapter, this);
          this._signalConnected = true;
        }
      } else {
        completionThemeManager.set_theme(null);
        delete document.body.dataset.lspCompleterLayout;
      }
      if (
        this.current_completion_handler &&
        this.model instanceof LSPCompleterModel
      ) {
        this.model.settings.caseSensitive =
          this.settings.composite.caseSensitive;
        this.model.settings.includePerfectMatches =
          this.settings.composite.includePerfectMatches;
        this.model.settings.preFilterMatches =
          this.settings.composite.preFilterMatches;
      }
    });
  }

  protected fetchDocumentation(item: LazyCompletionItem): void {
    if (!item) {
      return;
    }
    item
      .resolve()
      .then(resolvedCompletionItem => {
        if (item.self !== this._latestActiveItem!.self) {
          return;
        }
        this.set_doc_panel_placeholder(false);
        if (resolvedCompletionItem === null) {
          return;
        }
        this.refresh_doc_panel(item);
      })
      .catch(e => {
        // disabling placeholder can remove currently displayed documentation,
        // so only do that if this is really the active item!
        if (item.self === this._latestActiveItem!.self) {
          this.set_doc_panel_placeholder(false);
        }
        this.console.warn(e);
      });
  }

  active_completion_changed(
    renderer: LSPCompletionRenderer,
    active_completion: ICompletionData
  ) {
    let { item } = active_completion;
    this._latestActiveItem = item;
    if (!item.supportsResolution()) {
      if (item.isDocumentationMarkdown) {
        // TODO: remove once https://github.com/jupyterlab/jupyterlab/pull/9663 is merged and released
        this.refresh_doc_panel(item);
      }
      return;
    }

    if (item.needsResolution()) {
      this.set_doc_panel_placeholder(true);
      this.fetchDocumentation(item);
    } else if (item.isResolved()) {
      this.refresh_doc_panel(item);
    } else {
      // resolution has already started, but the re-render update could have been invalidated
      // by user action, so let's ensure the documentation will get shown this time.
      this.fetchDocumentation(item);
    }

    // also fetch completion for the previous and the next item to prevent jitter
    const index = this.current_index;
    const items = this.current_items;

    if (index - 1 >= 0) {
      const previous = items[index - 1] as LazyCompletionItem;
      this.resolve_and_update_from_item(previous?.self);
    }
    if (index + 1 < items.length) {
      const next = items[index + 1] as LazyCompletionItem;
      this.resolve_and_update_from_item(next?.self);
    }
  }

  private resolve_and_update_from_item(item: LazyCompletionItem) {
    if (!item) {
      return;
    }
    this.resolve_and_update(this.renderer, {
      item: item,
      element: item.element
    });
  }

  private resolve_and_update(
    renderer: LSPCompletionRenderer,
    active_completion: ICompletionData
  ) {
    let { item, element } = active_completion;
    if (!item.supportsResolution()) {
      this.renderer.updateExtraInfo(item, element);
      return;
    }

    if (item.isResolved()) {
      this.renderer.updateExtraInfo(item, element);
    } else {
      // supportsResolution as otherwise would short-circuit above
      item
        .resolve()
        .then(resolvedCompletionItem => {
          this.renderer.updateExtraInfo(item, element);
        })
        .catch(e => {
          this.console.warn(e);
        });
    }
  }

  private swap_adapter(
    manager: ILSPAdapterManager,
    adapter: WidgetAdapter<IDocumentWidget>
  ) {
    if (this.current_adapter) {
      // disconnect signals from the old adapter
      this.current_adapter.activeEditorChanged.disconnect(
        this.set_connector,
        this
      );
      this.current_adapter.adapterConnected.disconnect(
        this.connect_completion,
        this
      );
    }
    this.current_adapter = adapter;
    // connect the new adapter
    if (this.current_adapter.isConnected) {
      this.connect_completion(this.current_adapter);
      // TODO: what to do if adapter.activeEditor was just deleted/there is none because focus shifted?
      this.set_connector(adapter, { editor: adapter.activeEditor! });
    }
    // connect signals to the new adapter
    this.current_adapter.activeEditorChanged.connect(this.set_connector, this);
    this.current_adapter.adapterConnected.connect(
      this.connect_completion,
      this
    );
  }

  connect_completion(
    adapter: WidgetAdapter<IDocumentWidget>,
    data?: IDocumentConnectionData
  ) {
    let editor = adapter.activeEditor;
    if (editor == null) {
      return;
    }
    this.set_completion_connector(adapter, editor);
    this.current_completion_handler = this.completionManager.register(
      {
        connector: this.current_completion_connector,
        editor: editor,
        parent: adapter.widget
      },
      this.renderer
    ) as CompletionHandler;
    let completer = this.completer;
    if (this._disabled) {
      completer.removeClass(LSP_COMPLETER_CLASS);
      completer.model = new CompleterModel();
    } else {
      completer.addClass(LSP_COMPLETER_CLASS);
      completer.model = new LSPCompleterModel({
        caseSensitive: this.settings.composite.caseSensitive,
        includePerfectMatches: this.settings.composite.includePerfectMatches,
        preFilterMatches: this.settings.composite.preFilterMatches
      });
    }
  }

  protected get completer() {
    // TODO upstream: make completer public?
    return this.current_completion_handler.completer;
  }

  protected get model(): LSPCompleterModel | CompleterModel {
    return this.completer.model as LSPCompleterModel | CompleterModel;
  }

  invoke_completer(kind: ExtendedCompletionTriggerKind) {
    // TODO: ideally this would not re-trigger if list of items not isIncomplete
    let command: string;
    this.current_completion_connector.trigger_kind = kind;

    if (this.adapterManager.currentAdapter instanceof NotebookAdapter) {
      command = 'completer:invoke-notebook';
    } else {
      command = 'completer:invoke-file';
    }
    return this.app.commands.execute(command).catch(() => {
      this.current_completion_connector.trigger_kind =
        CompletionTriggerKind.Invoked;
    });
  }

  set_connector(
    adapter: WidgetAdapter<IDocumentWidget>,
    editor_changed: IEditorChangedData
  ) {
    if (!this.current_completion_handler) {
      // workaround for current_completion_handler not being there yet
      this.connect_completion(adapter);
    }
    this.set_completion_connector(adapter, editor_changed.editor);
    this.current_completion_handler.editor = editor_changed.editor;
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    this.current_completion_handler.connector =
      this.current_completion_connector;
  }

  private get current_items() {
    // TODO upstream: allow to get completionItems() without markup
    //   (note: not trivial as _markup() does filtering too)
    if (!(this.model instanceof LSPCompleterModel)) {
      return [];
    }
    return this.model.completionItems();
  }

  private get current_index() {
    let completer = this.current_completion_handler.completer;

    // TODO: use public activeIndex available since 3.1
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    return completer._activeIndex;
  }

  refresh_doc_panel(item: LazyCompletionItem) {
    let completer = this.current_completion_handler.completer;

    const active: CompletionHandler.ICompletionItem =
      this.current_items[this.current_index];

    if (!item || !active || active.insertText != item.insertText) {
      return;
    }

    const docPanel = completer.node.querySelector(DOC_PANEL_SELECTOR);
    if (!docPanel) {
      this.console.warn('Could not find completer panel to refresh');
      return;
    }
    docPanel.classList.remove(DOC_PANEL_PLACEHOLDER_CLASS);

    if (item.documentation) {
      // remove all children
      docPanel.textContent = '';
      // TODO upstream: renderer should take care of the documentation rendering
      //  sent PR: https://github.com/jupyterlab/jupyterlab/pull/9663

      const node = this.renderer.createDocumentationNode(item);
      docPanel.appendChild(node);

      docPanel.setAttribute('style', '');
    } else {
      docPanel.setAttribute('style', 'display: none');
    }
  }

  set_doc_panel_placeholder(enable: boolean): void {
    let completer = this.current_completion_handler.completer;
    const docPanel = completer.node.querySelector(DOC_PANEL_SELECTOR);
    if (!docPanel) {
      this.console.warn('Could not find completer panel for placeholder');
      return;
    }
    if (enable) {
      docPanel.setAttribute('style', '');
      docPanel.classList.add(DOC_PANEL_PLACEHOLDER_CLASS);
    } else if (docPanel.classList.contains(DOC_PANEL_PLACEHOLDER_CLASS)) {
      docPanel.setAttribute('style', 'display: none');
      docPanel.classList.remove(DOC_PANEL_PLACEHOLDER_CLASS);
    }
  }

  private set_completion_connector(
    adapter: WidgetAdapter<IDocumentWidget>,
    editor: CodeEditor.IEditor
  ) {
    this.current_completion_connector = new LSPConnector({
      editor: editor,
      themeManager: this.completionThemeManager,
      connections: this.current_adapter!.connection_manager.connections,
      virtual_editor: this.current_adapter!.virtual_editor,
      settings: this.settings,
      labIntegration: this,
      // it might or might not be a notebook panel (if it is not, the sessionContext and session will just be undefined)
      session: (this.current_adapter!.widget as NotebookPanel)?.sessionContext
        ?.session,
      console: this.console
    });
  }
}
