import type { UIEventStateContext } from '@blockeditor/block-std';
import {
  assertExists,
  DisposableGroup,
  throttle,
} from '@blockeditor/global/utils';
import type { EditorHost } from '@blockeditor/lit';
import { WidgetElement } from '@blockeditor/lit';
import type { BlockModel } from '@blockeditor/store';
import { customElement } from 'lit/decorators.js';

import { isControlledKeyboardEvent } from '../../../_common/utils/event.js';
import { matchFlavours } from '../../../_common/utils/index.js';
import {
  getInlineEditorByModel,
  getViewportElement,
} from '../../../_common/utils/query.js';
import { getCurrentNativeRange } from '../../../_common/utils/selection.js';
import { getPopperPosition } from '../../../page-block/utils/position.js';
import { getMenus, type LinkedDocOptions } from './config.js';
import { LinkedDocPopover } from './linked-doc-popover.js';

export function showLinkedDocPopover({
  editorHost,
  model,
  range,
  container = document.body,
  abortController = new AbortController(),
  options,
  triggerKey,
}: {
  editorHost: EditorHost;
  model: BlockModel;
  range: Range;
  container?: HTMLElement;
  abortController?: AbortController;
  options: LinkedDocOptions;
  triggerKey: string;
}) {
  const disposables = new DisposableGroup();
  abortController.signal.addEventListener('abort', () => disposables.dispose());

  const linkedDoc = new LinkedDocPopover(editorHost, model, abortController);
  linkedDoc.options = options;
  linkedDoc.triggerKey = triggerKey;
  // Mount
  container.appendChild(linkedDoc);
  disposables.add(() => linkedDoc.remove());

  // Handle position
  const updatePosition = throttle(() => {
    const linkedDocElement = linkedDoc.linkedDocElement;
    assertExists(
      linkedDocElement,
      'You should render the linked page node even if no position'
    );
    const position = getPopperPosition(linkedDocElement, range);
    linkedDoc.updatePosition(position);
  }, 10);
  disposables.addFromEvent(window, 'resize', updatePosition);
  const scrollContainer = getViewportElement(editorHost);
  if (scrollContainer) {
    // Note: in edgeless mode, the scroll container is not exist!
    disposables.addFromEvent(scrollContainer, 'scroll', updatePosition, {
      passive: true,
    });
  }

  // Wait for node to be mounted
  setTimeout(updatePosition);

  disposables.addFromEvent(window, 'mousedown', (e: Event) => {
    if (e.target === linkedDoc) return;
    abortController.abort();
  });

  return linkedDoc;
}

export const AFFINE_LINKED_DOC_WIDGET = 'affine-linked-doc-widget';

@customElement(AFFINE_LINKED_DOC_WIDGET)
export class AffineLinkedDocWidget extends WidgetElement {
  static DEFAULT_OPTIONS: LinkedDocOptions = {
    /**
     * The first item of the trigger keys will be the primary key
     */
    triggerKeys: ['@', '[[', '【【'],
    ignoreBlockTypes: ['affine:code'],
    /**
     * Convert trigger key to primary key (the first item of the trigger keys)
     */
    convertTriggerKey: true,
    getMenus,
  };

  options = AffineLinkedDocWidget.DEFAULT_OPTIONS;

  override connectedCallback() {
    super.connectedCallback();
    this.handleEvent('keyDown', this._onKeyDown);
  }

  public showLinkedDoc = (model: BlockModel, triggerKey: string) => {
    const curRange = getCurrentNativeRange();
    showLinkedDocPopover({
      editorHost: this.host,
      model,
      range: curRange,
      options: this.options,
      triggerKey,
    });
  };

  private _onKeyDown = (ctx: UIEventStateContext) => {
    const eventState = ctx.get('keyboardState');
    const event = eventState.raw;
    if (isControlledKeyboardEvent(event) || event.key.length !== 1) return;
    const text = this.host.selection.value.find(selection =>
      selection.is('text')
    );
    if (!text) {
      return;
    }
    const model = this.host.page.getBlockById(text.blockId);
    if (!model) {
      return;
    }
    if (matchFlavours(model, this.options.ignoreBlockTypes)) return;
    const inlineEditor = getInlineEditorByModel(this.host, model);
    if (!inlineEditor) return;
    const inlineRange = inlineEditor.getInlineRange();
    if (!inlineRange) return;
    if (inlineRange.length > 0) {
      // When select text and press `[[` should not trigger transform,
      // since it will break the bracket complete.
      // Expected `[[selected text]]` instead of `@selected text]]`
      return;
    }

    const [leafStart, offsetStart] = inlineEditor.getTextPoint(
      inlineRange.index
    );
    const prefixText = leafStart.textContent
      ? leafStart.textContent.slice(0, offsetStart)
      : '';

    const matchedKey = this.options.triggerKeys.find(triggerKey =>
      (prefixText + event.key).endsWith(triggerKey)
    );
    if (!matchedKey) return;

    const primaryTriggerKey = this.options.triggerKeys[0];
    inlineEditor.slots.inlineRangeApply.once(() => {
      if (this.options.convertTriggerKey && primaryTriggerKey !== matchedKey) {
        // Convert to the primary trigger key
        // e.g. [[ -> @
        const startIdxBeforeMatchKey =
          inlineRange.index - (matchedKey.length - 1);
        inlineEditor.deleteText({
          index: startIdxBeforeMatchKey,
          length: matchedKey.length,
        });
        inlineEditor.insertText(
          { index: startIdxBeforeMatchKey, length: 0 },
          primaryTriggerKey
        );
        inlineEditor.setInlineRange({
          index: startIdxBeforeMatchKey + primaryTriggerKey.length,
          length: 0,
        });
        inlineEditor.slots.inlineRangeApply.once(() => {
          this.showLinkedDoc(model, primaryTriggerKey);
        });
        return;
      }
      this.showLinkedDoc(model, matchedKey);
    });
  };
}

declare global {
  interface HTMLElementTagNameMap {
    [AFFINE_LINKED_DOC_WIDGET]: AffineLinkedDocWidget;
  }
}
