import {Extension, Extensions, getTextBetween, posToDOMRect} from "@tiptap/core";
import {AiEditor} from "./AiEditor.ts";
import {BubbleMenuOptions, BubbleMenuPlugin} from "../components/bubbles/BubbleMenuPlugin.ts";

import {LinkBubbleMenu} from "../components/bubbles/LinkBubbleMenu.ts";
import {AbstractBubbleMenu} from "../components/AbstractBubbleMenu.ts";
import {ImageBubbleMenu} from "../components/bubbles/ImageBubbleMenu.ts";
import {TableBubbleMenu} from "../components/bubbles/TableBubbleMenu.ts";
import {TextSelectionBubbleMenu} from "../components/bubbles/TextSelectionBubbleMenu.ts";
import {Instance} from "tippy.js";
import {defineCustomElement} from "../commons/defineCustomElement.ts";
import {CellSelection} from "@tiptap/pm/tables";
import {getAIBoundingClientRect} from "../util/getAIBoundingClientRect.ts";

defineCustomElement('linwiteditor-bubble-link', LinkBubbleMenu);
defineCustomElement('linwiteditor-bubble-image', ImageBubbleMenu);
defineCustomElement('linwiteditor-bubble-table', TableBubbleMenu);
defineCustomElement('linwiteditor-bubble-text', TextSelectionBubbleMenu);


function createBubbleMenu(name: string, options: BubbleMenuOptions) {
    return Extension.create<BubbleMenuOptions>({
        name: name,
        addOptions() {
            return {
                ...options
            }
        },
        addProseMirrorPlugins() {
            if (!this.options.element) {
                return []
            }

            return [
                BubbleMenuPlugin({
                    pluginKey: this.options.pluginKey,
                    editor: this.editor,
                    element: this.options.element,
                    tippyOptions: this.options.tippyOptions,
                    updateDelay: this.options.updateDelay,
                    shouldShow: this.options.shouldShow,
                    updateAtMouseUp: this.options.updateAtMouseUp,
                }),
            ]
        },
    })
}


const createTextSelectionBubbleMenu = (linwitDocEditor: AiEditor) => {
    const elementTagName = linwitDocEditor.options.textSelectionBubbleMenu?.elementTagName || "linwiteditor-bubble-text";
    const menuEl = document.createElement(elementTagName) as TextSelectionBubbleMenu;
    linwitDocEditor.eventComponents.push(menuEl);

    return createBubbleMenu("textSelectionBubble", {
        pluginKey: 'textSelectionBubble',
        element: menuEl,
        updateDelay: 0,
        updateAtMouseUp: true,
        tippyOptions: {
            appendTo: linwitDocEditor.container,
            arrow: false,
            placement: 'bottom',
            getReferenceClientRect: () => getAIBoundingClientRect(linwitDocEditor.innerEditor),
            onCreate(instance: Instance) {
                menuEl.instance = instance;
            }
        },
        shouldShow: ({editor}) => {
            if (!editor.isEditable) {
                return false;
            }
            const {state: {selection}} = editor;
            return !selection.empty && getTextBetween(editor.state.doc, {
                    from: selection.from,
                    to: selection.to
                }).trim().length > 0
                && !editor.isActive("link")
                && !editor.isActive("image")
                && !editor.isActive("codeBlock")
                // 选中表格的时候取消 文本的弹出
                && !(selection instanceof CellSelection)
        }
    })
}


const createLinkBubbleMenu = (linwitDocEditor: AiEditor) => {
    const menuEl = document.createElement("linwiteditor-bubble-link") as AbstractBubbleMenu;
    linwitDocEditor.eventComponents.push(menuEl);
    return createBubbleMenu("linkBubble", {
        pluginKey: 'linkBubble',
        element: menuEl,
        tippyOptions: {
            appendTo: linwitDocEditor.container,
            placement: 'bottom',
            arrow: false,
            onCreate(instance: Instance) {
                menuEl.instance = instance;
            },
        },
        shouldShow: ({editor}) => {
            return editor.isEditable && editor.isActive("link")
        }
    })
}


const createImageBubbleMenu = (linwitDocEditor: AiEditor) => {
    const menuEl = document.createElement("linwiteditor-bubble-image") as AbstractBubbleMenu;
    linwitDocEditor.eventComponents.push(menuEl);
    return createBubbleMenu("imageBubble", {
        pluginKey: 'imageBubble',
        element: menuEl,
        tippyOptions: {
            appendTo: linwitDocEditor.container,
            placement: 'top-start',
            arrow: false,
            getReferenceClientRect: (() => {
                const {ranges} = linwitDocEditor.innerEditor.state.selection
                const from = Math.min(...ranges.map(range => range.$from.pos))
                const to = Math.max(...ranges.map(range => range.$to.pos))
                const {view} = linwitDocEditor.innerEditor;

                let node = view.nodeDOM(from) as HTMLElement
                const imageEl = node.querySelector("img") as HTMLImageElement;

                const domRect = posToDOMRect(view, from, to);
                const imgRect = imageEl.getBoundingClientRect();
                return {
                    ...domRect,
                    left: imgRect.left + imgRect.width * 0.5 - 100
                }
            }),
            onCreate(instance: Instance) {
                menuEl.instance = instance;
            },
        },
        shouldShow: ({editor}) => {
            return editor.isEditable && editor.isActive("image")
        }
    })
}


const createTableBubbleMenu = (linwitDocEditor: AiEditor) => {
    const menuEl = document.createElement("linwiteditor-bubble-table") as AbstractBubbleMenu;
    linwitDocEditor.eventComponents.push(menuEl);
    return createBubbleMenu("tableBubble", {
        pluginKey: 'tableBubble',
        element: menuEl,
        tippyOptions: {
            placement: 'top',
            appendTo: linwitDocEditor.container,
            arrow: false,
            getReferenceClientRect: (() => {
                const selection = linwitDocEditor.innerEditor.state.selection;
                const {ranges} = selection
                const from = Math.min(...ranges.map(range => range.$from.pos))
                const to = Math.max(...ranges.map(range => range.$to.pos))
                const {view, state} = linwitDocEditor.innerEditor;

                const domRect = posToDOMRect(view, from, to);
                const tablePos = state.selection.$from.posAtIndex(0, 1);
                const coordsAtPos = view.coordsAtPos(tablePos);

                return {
                    ...domRect,
                    top: coordsAtPos.top
                };
            })
        },
        shouldShow: ({editor}) => {
            const {state: {selection}} = editor;
            return editor.isEditable && editor.isActive("table") && selection instanceof CellSelection
        }
    })
}


export const getBubbleMenus = (linwitDocEditor: AiEditor): Extensions => {
    const bubbleMenus: Extensions = [];
    if (linwitDocEditor.options.editable === false) {
        return bubbleMenus;
    }

    const textSelectionEnable = !(linwitDocEditor?.options.textSelectionBubbleMenu?.enable === false)
    if (textSelectionEnable) {
        bubbleMenus.push(createTextSelectionBubbleMenu(linwitDocEditor))
    }

    bubbleMenus.push(createLinkBubbleMenu(linwitDocEditor))

    const imageBubbleMenuEnable = !(linwitDocEditor.options.image?.bubbleMenuEnable === false)
    if (imageBubbleMenuEnable) {
        bubbleMenus.push(createImageBubbleMenu(linwitDocEditor))
    }

    bubbleMenus.push(createTableBubbleMenu(linwitDocEditor))
    return bubbleMenus;
}
