import { useLexicalComposerContext } from "./LexicalComposerContext";
import { eventFiles } from "@lexical/rich-text";
import { calculateZoomLevel, isHTMLElement, mergeRegister, } from "@lexical/utils";
import { $getNearestNodeFromDOMNode, $getNodeByKey, $getRoot, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, DRAGOVER_COMMAND, DROP_COMMAND, } from "lexical";
import { createEffect, createSignal, onCleanup } from "solid-js";
import { Portal } from "solid-js/web";
import { Point } from "./shared/point";
import { Rectangle } from "./shared/rect";
const SPACE = 4;
const TARGET_LINE_HALF_HEIGHT = 2;
const DRAG_DATA_FORMAT = "application/x-lexical-drag-block";
const TEXT_BOX_HORIZONTAL_PADDING = 28;
const Downward = 1;
const Upward = -1;
const Indeterminate = 0;
let prevIndex = Infinity;
function getCurrentIndex(keysLength) {
    if (keysLength === 0) {
        return Infinity;
    }
    if (prevIndex >= 0 && prevIndex < keysLength) {
        return prevIndex;
    }
    return Math.floor(keysLength / 2);
}
function getTopLevelNodeKeys(editor) {
    return editor.getEditorState().read(() => $getRoot().getChildrenKeys());
}
function getCollapsedMargins(elem) {
    const getMargin = (element, margin) => element ? parseFloat(window.getComputedStyle(element)[margin]) : 0;
    const { marginTop, marginBottom } = window.getComputedStyle(elem);
    const prevElemSiblingMarginBottom = getMargin(elem.previousElementSibling, "marginBottom");
    const nextElemSiblingMarginTop = getMargin(elem.nextElementSibling, "marginTop");
    const collapsedTopMargin = Math.max(parseFloat(marginTop), prevElemSiblingMarginBottom);
    const collapsedBottomMargin = Math.max(parseFloat(marginBottom), nextElemSiblingMarginTop);
    return { marginBottom: collapsedBottomMargin, marginTop: collapsedTopMargin };
}
function getBlockElement(anchorElem, editor, event, useEdgeAsDefault = false) {
    const anchorElementRect = anchorElem.getBoundingClientRect();
    const topLevelNodeKeys = getTopLevelNodeKeys(editor);
    let blockElem = null;
    editor.getEditorState().read(() => {
        if (useEdgeAsDefault) {
            const [firstNode, lastNode] = [
                editor.getElementByKey(topLevelNodeKeys[0]),
                editor.getElementByKey(topLevelNodeKeys[topLevelNodeKeys.length - 1]),
            ];
            const [firstNodeRect, lastNodeRect] = [
                firstNode != null ? firstNode.getBoundingClientRect() : undefined,
                lastNode != null ? lastNode.getBoundingClientRect() : undefined,
            ];
            if (firstNodeRect && lastNodeRect) {
                const firstNodeZoom = calculateZoomLevel(firstNode);
                const lastNodeZoom = calculateZoomLevel(lastNode);
                if (event.y / firstNodeZoom < firstNodeRect.top) {
                    blockElem = firstNode;
                }
                else if (event.y / lastNodeZoom > lastNodeRect.bottom) {
                    blockElem = lastNode;
                }
                if (blockElem) {
                    return;
                }
            }
        }
        let index = getCurrentIndex(topLevelNodeKeys.length);
        let direction = Indeterminate;
        while (index >= 0 && index < topLevelNodeKeys.length) {
            const key = topLevelNodeKeys[index];
            const elem = editor.getElementByKey(key);
            if (elem === null) {
                break;
            }
            const zoom = calculateZoomLevel(elem);
            const point = new Point(event.x / zoom, event.y / zoom);
            const domRect = Rectangle.fromDOM(elem);
            const { marginTop, marginBottom } = getCollapsedMargins(elem);
            const rect = domRect.generateNewRect({
                bottom: domRect.bottom + marginBottom,
                left: anchorElementRect.left,
                right: anchorElementRect.right,
                top: domRect.top - marginTop,
            });
            const { result, reason: { isOnTopSide, isOnBottomSide }, } = rect.contains(point);
            if (result) {
                blockElem = elem;
                prevIndex = index;
                break;
            }
            if (direction === Indeterminate) {
                if (isOnTopSide) {
                    direction = Upward;
                }
                else if (isOnBottomSide) {
                    direction = Downward;
                }
                else {
                    // stop search block element
                    direction = Infinity;
                }
            }
            index += direction;
        }
    });
    return blockElem;
}
function setMenuPosition(targetElem, floatingElem, anchorElem) {
    if (!targetElem) {
        floatingElem.style.opacity = "0";
        floatingElem.style.transform = "translate(-10000px, -10000px)";
        return;
    }
    const targetRect = targetElem.getBoundingClientRect();
    const targetStyle = window.getComputedStyle(targetElem);
    const floatingElemRect = floatingElem.getBoundingClientRect();
    const anchorElementRect = anchorElem.getBoundingClientRect();
    // top left
    let targetCalculateHeight = parseInt(targetStyle.lineHeight, 10);
    if (isNaN(targetCalculateHeight)) {
        // middle
        targetCalculateHeight = targetRect.bottom - targetRect.top;
    }
    const top = targetRect.top +
        (targetCalculateHeight - floatingElemRect.height) / 2 -
        anchorElementRect.top;
    const left = SPACE;
    floatingElem.style.opacity = "1";
    floatingElem.style.transform = `translate(${left}px, ${top}px)`;
}
function setDragImage(dataTransfer, draggableBlockElem) {
    const { transform } = draggableBlockElem.style;
    // Remove dragImage borders
    draggableBlockElem.style.transform = "translateZ(0)";
    dataTransfer.setDragImage(draggableBlockElem, 0, 0);
    setTimeout(() => {
        draggableBlockElem.style.transform = transform;
    });
}
function setTargetLine(targetLineElem, targetBlockElem, mouseY, anchorElem) {
    const { top: targetBlockElemTop, height: targetBlockElemHeight } = targetBlockElem.getBoundingClientRect();
    const { top: anchorTop, width: anchorWidth } = anchorElem.getBoundingClientRect();
    const { marginTop, marginBottom } = getCollapsedMargins(targetBlockElem);
    let lineTop = targetBlockElemTop;
    if (mouseY >= targetBlockElemTop) {
        lineTop += targetBlockElemHeight + marginBottom / 2;
    }
    else {
        lineTop -= marginTop / 2;
    }
    const top = lineTop - anchorTop - TARGET_LINE_HALF_HEIGHT;
    const left = TEXT_BOX_HORIZONTAL_PADDING - SPACE;
    targetLineElem.style.transform = `translate(${left}px, ${top}px)`;
    targetLineElem.style.width = `${anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2}px`;
    targetLineElem.style.opacity = ".4";
}
function hideTargetLine(targetLineElem) {
    if (targetLineElem) {
        targetLineElem.style.opacity = "0";
        targetLineElem.style.transform = "translate(-10000px, -10000px)";
    }
}
function useDraggableBlockMenu(editor, anchorElem, menuRef, targetLineRef, isEditable, menuComponent, targetLineComponent, isOnMenu, onElementChanged) {
    const scrollerElem = anchorElem.parentElement;
    const isDraggingBlockRef = { current: false };
    const [draggableBlockElem, setDraggableBlockElemState] = createSignal(null);
    const setDraggableBlockElem = (elem) => {
        setDraggableBlockElemState(elem);
        if (onElementChanged) {
            onElementChanged(elem);
        }
    };
    createEffect(() => {
        function onMouseMove(event) {
            const target = event.target;
            if (!isHTMLElement(target) && !isHTMLElement(target)) {
                setDraggableBlockElem(null);
                return;
            }
            if (isOnMenu(target)) {
                return;
            }
            const _draggableBlockElem = getBlockElement(anchorElem, editor, event);
            setDraggableBlockElem(_draggableBlockElem);
        }
        function onMouseLeave() {
            setDraggableBlockElem(null);
        }
        if (scrollerElem != null) {
            scrollerElem.addEventListener("mousemove", onMouseMove);
            scrollerElem.addEventListener("mouseleave", onMouseLeave);
        }
        return () => {
            if (scrollerElem != null) {
                scrollerElem.removeEventListener("mousemove", onMouseMove);
                scrollerElem.removeEventListener("mouseleave", onMouseLeave);
            }
        };
    }, [scrollerElem, anchorElem, editor, isOnMenu]);
    createEffect(() => {
        const menuRefUnwrapped = menuRef();
        if (menuRefUnwrapped) {
            setMenuPosition(draggableBlockElem(), menuRefUnwrapped, anchorElem);
        }
    });
    createEffect(() => {
        function onDragover(event) {
            if (!isDraggingBlockRef.current) {
                return false;
            }
            const [isFileTransfer] = eventFiles(event);
            if (isFileTransfer) {
                return false;
            }
            const { pageY, target } = event;
            if (!isHTMLElement(target) && !isHTMLElement(target)) {
                return false;
            }
            const targetBlockElem = getBlockElement(anchorElem, editor, event, true);
            const targetLineElem = targetLineRef();
            if (targetBlockElem === null || targetLineElem === null) {
                return false;
            }
            setTargetLine(targetLineElem, targetBlockElem, pageY / calculateZoomLevel(target), anchorElem);
            // Prevent default event to be able to trigger onDrop events
            event.preventDefault();
            return true;
        }
        function $onDrop(event) {
            if (!isDraggingBlockRef.current) {
                return false;
            }
            const [isFileTransfer] = eventFiles(event);
            if (isFileTransfer) {
                return false;
            }
            const { target, dataTransfer, pageY } = event;
            const dragData = dataTransfer != null ? dataTransfer.getData(DRAG_DATA_FORMAT) : "";
            const draggedNode = $getNodeByKey(dragData);
            if (!draggedNode) {
                return false;
            }
            if (!isHTMLElement(target)) {
                return false;
            }
            const targetBlockElem = getBlockElement(anchorElem, editor, event, true);
            if (!targetBlockElem) {
                return false;
            }
            const targetNode = $getNearestNodeFromDOMNode(targetBlockElem);
            if (!targetNode) {
                return false;
            }
            if (targetNode === draggedNode) {
                return true;
            }
            const targetBlockElemTop = targetBlockElem.getBoundingClientRect().top;
            if (pageY / calculateZoomLevel(target) >= targetBlockElemTop) {
                targetNode.insertAfter(draggedNode);
            }
            else {
                targetNode.insertBefore(draggedNode);
            }
            setDraggableBlockElem(null);
            return true;
        }
        onCleanup(mergeRegister(editor.registerCommand(DRAGOVER_COMMAND, (event) => {
            return onDragover(event);
        }, COMMAND_PRIORITY_LOW), editor.registerCommand(DROP_COMMAND, (event) => {
            return $onDrop(event);
        }, COMMAND_PRIORITY_HIGH)));
    });
    function onDragStart(event) {
        const dataTransfer = event.dataTransfer;
        const draggableBlockElemUnwrapped = draggableBlockElem();
        if (!dataTransfer || !draggableBlockElemUnwrapped) {
            return;
        }
        setDragImage(dataTransfer, draggableBlockElemUnwrapped);
        let nodeKey = "";
        editor.update(() => {
            const node = $getNearestNodeFromDOMNode(draggableBlockElemUnwrapped);
            if (node) {
                nodeKey = node.getKey();
            }
        });
        isDraggingBlockRef.current = true;
        dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey);
    }
    function onDragEnd() {
        isDraggingBlockRef.current = false;
        hideTargetLine(targetLineRef());
    }
    return (<Portal mount={anchorElem}>
      <div draggable={true} onDragStart={onDragStart} onDragEnd={onDragEnd}>
        {isEditable && menuComponent}
      </div>
      {targetLineComponent}
    </Portal>);
}
export function DraggableBlockPlugin_EXPERIMENTAL(props) {
    const [editor] = useLexicalComposerContext();
    return useDraggableBlockMenu(editor, props.anchorElem || document.body, () => props.menuRef || null, () => props.targetLineRef || null, editor._editable, props.menuComponent, props.targetLineComponent, props.isOnMenu, props.onElementChanged);
}
