import { useLexicalComposerContext } from "../LexicalComposerContext";
import { CAN_USE_DOM, mergeRegister } from "@lexical/utils";
import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, createCommand, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND, } from "lexical";
import { createEffect, createMemo, createSignal, on, onCleanup, untrack, } from "solid-js";
export const PUNCTUATION = "\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%'\"~=<>_:;";
export class MenuOption {
    key;
    ref;
    constructor(key) {
        this.key = key;
        this.ref = { current: null };
        this.setRefElement = this.setRefElement.bind(this);
    }
    setRefElement(element) {
        this.ref = { current: element };
    }
}
const scrollIntoViewIfNeeded = (target) => {
    const typeaheadContainerNode = document.getElementById("typeahead-menu");
    if (!typeaheadContainerNode) {
        return;
    }
    const typeaheadRect = typeaheadContainerNode.getBoundingClientRect();
    if (typeaheadRect.top + typeaheadRect.height > window.innerHeight) {
        typeaheadContainerNode.scrollIntoView({
            block: "center",
        });
    }
    if (typeaheadRect.top < 0) {
        typeaheadContainerNode.scrollIntoView({
            block: "center",
        });
    }
    target.scrollIntoView({ block: "nearest" });
};
/**
 * Walk backwards along user input and forward through entity title to try
 * and replace more of the user's text with entity.
 */
function getFullMatchOffset(documentText, entryText, offset) {
    let triggerOffset = offset;
    for (let i = triggerOffset; i <= entryText.length; i++) {
        if (documentText.slice(-i) === entryText.substring(0, i)) {
            triggerOffset = i;
        }
    }
    return triggerOffset;
}
/**
 * Split Lexical TextNode and return a new TextNode only containing matched text.
 * Common use cases include: removing the node, replacing with a new node.
 */
function $splitNodeContainingQuery(match) {
    const selection = $getSelection();
    if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
        return null;
    }
    const anchor = selection.anchor;
    if (anchor.type !== "text") {
        return null;
    }
    const anchorNode = anchor.getNode();
    if (!anchorNode.isSimpleText()) {
        return null;
    }
    const selectionOffset = anchor.offset;
    const textContent = anchorNode.getTextContent().slice(0, selectionOffset);
    const characterOffset = match.replaceableString.length;
    const queryOffset = getFullMatchOffset(textContent, match.matchingString, characterOffset);
    const startOffset = selectionOffset - queryOffset;
    if (startOffset < 0) {
        return null;
    }
    let newNode;
    if (startOffset === 0) {
        [newNode] = anchorNode.splitText(selectionOffset);
    }
    else {
        [, newNode] = anchorNode.splitText(startOffset, selectionOffset);
    }
    return newNode;
}
// Got from https://stackoverflow.com/a/42543908/2013580
export function getScrollParent(element, includeHidden) {
    let style = getComputedStyle(element);
    const excludeStaticParent = style.position === "absolute";
    const overflowRegex = includeHidden
        ? /(auto|scroll|hidden)/
        : /(auto|scroll)/;
    if (style.position === "fixed") {
        return document.body;
    }
    for (let parent = element; (parent = parent.parentElement);) {
        style = getComputedStyle(parent);
        if (excludeStaticParent && style.position === "static") {
            continue;
        }
        if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) {
            return parent;
        }
    }
    return document.body;
}
function isTriggerVisibleInNearestScrollContainer(targetElement, containerElement) {
    const tRect = targetElement.getBoundingClientRect();
    const cRect = containerElement.getBoundingClientRect();
    return tRect.top > cRect.top && tRect.top < cRect.bottom;
}
// Reposition the menu on scroll, window resize, and element resize.
export function useDynamicPositioning(resolution, targetElement, onReposition, onVisibilityChange) {
    const [editor] = useLexicalComposerContext();
    createEffect(() => {
        if (targetElement != null && resolution() != null) {
            const rootElement = editor.getRootElement();
            const rootScrollParent = rootElement != null
                ? getScrollParent(rootElement, false)
                : document.body;
            let ticking = false;
            let previousIsInView = isTriggerVisibleInNearestScrollContainer(targetElement, rootScrollParent);
            const handleScroll = function () {
                if (!ticking) {
                    window.requestAnimationFrame(function () {
                        onReposition();
                        ticking = false;
                    });
                    ticking = true;
                }
                const isInView = isTriggerVisibleInNearestScrollContainer(targetElement, rootScrollParent);
                if (isInView !== previousIsInView) {
                    previousIsInView = isInView;
                    if (onVisibilityChange != null) {
                        onVisibilityChange(isInView);
                    }
                }
            };
            const resizeObserver = new ResizeObserver(onReposition);
            window.addEventListener("resize", onReposition);
            document.addEventListener("scroll", handleScroll, {
                capture: true,
                passive: true,
            });
            resizeObserver.observe(targetElement);
            onCleanup(() => {
                resizeObserver.unobserve(targetElement);
                window.removeEventListener("resize", onReposition);
                document.removeEventListener("scroll", handleScroll, true);
            });
        }
    });
}
export const SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND = createCommand("SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND");
export function LexicalMenu(props) {
    const [selectedIndex, setHighlightedIndex] = createSignal(null);
    createEffect(on(() => [
        props.preselectFirstItem,
        props.resolution.match && props.resolution.match.matchingString,
    ], (preselectFirstItem) => {
        if (preselectFirstItem)
            setHighlightedIndex(0);
    }));
    const selectOptionAndCleanUp = (selectedEntry) => {
        props.editor.update(() => {
            const textNodeContainingQuery = props.resolution.match != null && props.shouldSplitNodeWithQuery
                ? $splitNodeContainingQuery(props.resolution.match)
                : null;
            props.onSelectOption(selectedEntry, textNodeContainingQuery, props.close, props.resolution.match ? props.resolution.match.matchingString : "");
        });
    };
    const updateSelectedIndex = (index) => {
        const rootElem = props.editor.getRootElement();
        if (rootElem !== null) {
            rootElem.setAttribute("aria-activedescendant", "typeahead-item-" + index);
            setHighlightedIndex(index);
        }
    };
    createEffect(() => {
        onCleanup(() => {
            const rootElem = props.editor.getRootElement();
            if (rootElem !== null) {
                rootElem.removeAttribute("aria-activedescendant");
            }
        });
    });
    createEffect(() => {
        if (props.options === null) {
            setHighlightedIndex(null);
        }
        else if (selectedIndex() === null && props.preselectFirstItem) {
            updateSelectedIndex(0);
        }
    });
    createEffect(() => {
        onCleanup(() => mergeRegister(props.editor.registerCommand(SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND, ({ option }) => {
            if (option.ref && option.ref.current != null) {
                scrollIntoViewIfNeeded(option.ref.current);
                return true;
            }
            return false;
        }, props.commandPriority ?? COMMAND_PRIORITY_LOW)));
    });
    createEffect(() => {
        onCleanup(mergeRegister(props.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (payload) => {
            const event = payload;
            if (props.options !== null && props.options.length) {
                const newSelectedIndex = selectedIndex() === null
                    ? 0
                    : selectedIndex() !== props.options.length - 1
                        ? selectedIndex() + 1
                        : 0;
                updateSelectedIndex(newSelectedIndex);
                const option = props.options[newSelectedIndex];
                if (option.ref != null && option.ref.current) {
                    props.editor.dispatchCommand(SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND, {
                        index: newSelectedIndex,
                        option,
                    });
                }
                event.preventDefault();
                event.stopImmediatePropagation();
            }
            return true;
        }, props.commandPriority ?? COMMAND_PRIORITY_LOW), props.editor.registerCommand(KEY_ARROW_UP_COMMAND, (payload) => {
            const event = payload;
            if (props.options !== null && props.options.length) {
                const newSelectedIndex = selectedIndex() === null
                    ? props.options.length - 1
                    : selectedIndex() !== 0
                        ? selectedIndex() - 1
                        : props.options.length - 1;
                updateSelectedIndex(newSelectedIndex);
                const option = props.options[newSelectedIndex];
                if (option.ref != null && option.ref.current) {
                    scrollIntoViewIfNeeded(option.ref.current);
                }
                event.preventDefault();
                event.stopImmediatePropagation();
            }
            return true;
        }, props.commandPriority ?? COMMAND_PRIORITY_LOW), props.editor.registerCommand(KEY_ESCAPE_COMMAND, (payload) => {
            const event = payload;
            event.preventDefault();
            event.stopImmediatePropagation();
            close();
            return true;
        }, props.commandPriority ?? COMMAND_PRIORITY_LOW), props.editor.registerCommand(KEY_TAB_COMMAND, (payload) => {
            const event = payload;
            if (props.options === null ||
                selectedIndex() === null ||
                props.options[selectedIndex()] == null) {
                return false;
            }
            event.preventDefault();
            event.stopImmediatePropagation();
            selectOptionAndCleanUp(props.options[selectedIndex()]);
            return true;
        }, props.commandPriority ?? COMMAND_PRIORITY_LOW), props.editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
            if (props.options === null ||
                selectedIndex() === null ||
                props.options[selectedIndex()] == null) {
                return false;
            }
            if (event !== null) {
                event.preventDefault();
                event.stopImmediatePropagation();
            }
            selectOptionAndCleanUp(props.options[selectedIndex()]);
            return true;
        }, props.commandPriority ?? COMMAND_PRIORITY_LOW)));
    });
    const listItemProps = {
        options: props.options,
        selectOptionAndCleanUp,
        selectedIndex: createMemo(selectedIndex),
        setHighlightedIndex,
    };
    return untrack(() => props.menuRenderFn(props.anchorElementRef, listItemProps, props.resolution.match ? props.resolution.match.matchingString : ""));
}
function setContainerDivAttributes(containerDiv, className) {
    if (className != null) {
        containerDiv.className = className;
    }
    containerDiv.setAttribute("aria-label", "Typeahead menu");
    containerDiv.setAttribute("role", "listbox");
    containerDiv.style.display = "block";
    containerDiv.style.position = "absolute";
}
export function useMenuAnchorRef(resolution, setResolution, className, parent = CAN_USE_DOM ? document.body : undefined, shouldIncludePageYOffset__EXPERIMENTAL = true) {
    const [editor] = useLexicalComposerContext();
    let anchorElementRef = CAN_USE_DOM ? document.createElement("div") : null;
    const positionMenu = () => {
        if (anchorElementRef == null || parent == null)
            return;
        anchorElementRef.style.top = anchorElementRef.style.bottom;
        const rootElement = editor.getRootElement();
        const containerDiv = anchorElementRef;
        const menuEle = containerDiv.firstChild;
        if (rootElement !== null && resolution() !== null) {
            const { left, top, width, height } = resolution().getRect();
            const anchorHeight = anchorElementRef.offsetHeight; // use to position under anchor
            containerDiv.style.top = `${top +
                anchorHeight +
                3 +
                (shouldIncludePageYOffset__EXPERIMENTAL ? window.pageYOffset : 0)}px`;
            containerDiv.style.left = `${left + window.pageXOffset}px`;
            containerDiv.style.height = `${height}px`;
            containerDiv.style.width = `${width}px`;
            if (menuEle !== null) {
                menuEle.style.top = `${top}`;
                const menuRect = menuEle.getBoundingClientRect();
                const menuHeight = menuRect.height;
                const menuWidth = menuRect.width;
                const rootElementRect = rootElement.getBoundingClientRect();
                if (left + menuWidth > rootElementRect.right) {
                    containerDiv.style.left = `${left - menuWidth + window.pageXOffset}px`;
                }
                if ((top + menuHeight > window.innerHeight ||
                    top + menuHeight > rootElementRect.bottom) &&
                    top - rootElementRect.top > menuHeight + height) {
                    containerDiv.style.top = `${top -
                        menuHeight -
                        height +
                        (shouldIncludePageYOffset__EXPERIMENTAL ? window.pageYOffset : 0)}px`;
                }
            }
            if (!containerDiv.isConnected) {
                setContainerDivAttributes(containerDiv, className);
                parent.append(containerDiv);
            }
            containerDiv.setAttribute("id", "typeahead-menu");
            anchorElementRef = containerDiv;
            rootElement.setAttribute("aria-controls", "typeahead-menu");
        }
    };
    createEffect(() => {
        const rootElement = editor.getRootElement();
        if (resolution() !== null) {
            positionMenu();
        }
        onCleanup(() => {
            if (rootElement !== null) {
                rootElement.removeAttribute("aria-controls");
            }
            const containerDiv = anchorElementRef;
            if (containerDiv !== null && containerDiv.isConnected) {
                containerDiv.remove();
                containerDiv.removeAttribute("id");
            }
        });
    });
    const onVisibilityChange = (isInView) => {
        if (resolution() !== null) {
            if (!isInView) {
                setResolution(null);
            }
        }
    };
    useDynamicPositioning(resolution, anchorElementRef, positionMenu, onVisibilityChange);
    // Append the context for the menu immediately
    const containerDiv = anchorElementRef;
    if (containerDiv != null) {
        setContainerDivAttributes(containerDiv, className);
        if (parent != null) {
            parent.append(containerDiv);
        }
    }
    return {
        get current() {
            return anchorElementRef;
        },
    };
}
