import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useEffect, useRef, useState } from 'react';
import { useIsomorphicLayoutEffect } from '@selfcommunity/react-core';
import { $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND } from 'lexical';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { mergeRegister } from '@lexical/utils';
import { createMentionNode, MentionNode } from '../nodes/MentionNode';
import { http, Endpoints } from '@selfcommunity/api-services';
import classNames from 'classnames';
import { Avatar, Portal } from '@mui/material';
import { styled } from '@mui/material/styles';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import { PREFIX } from '../constants';
const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;';
const NAME = '\\b[A-Z][^\\s' + PUNCTUATION + ']';
const DocumentMentionsRegex = {
    NAME,
    PUNCTUATION
};
const CapitalizedNameMentionsRegex = new RegExp('(^|[^#])((?:' + DocumentMentionsRegex.NAME + '{' + 1 + ',})$)');
const PUNC = DocumentMentionsRegex.PUNCTUATION;
const TRIGGERS = ['@', '\\uff20'].join('');
// Chars we expect to see in a mention (non-space, non-punctuation).
const VALID_CHARS = '[^' + TRIGGERS + PUNC + '\\s]';
// Non-standard series of chars. Each series must be preceded and followed by
// a valid char.
const VALID_JOINS = '(?:' +
    '\\.[ |$]|' + // E.g. "r. " in "Mr. Smith"
    ' |' + // E.g. " " in "Josh Duck"
    '[' +
    PUNC +
    ']|' + // E.g. "-' in "Salier-Hellendag"
    ')';
const LENGTH_LIMIT = 75;
const AtSignMentionsRegex = new RegExp('(^|\\s|\\()(' + '[' + TRIGGERS + ']' + '((?:' + VALID_CHARS + VALID_JOINS + '){0,' + LENGTH_LIMIT + '})' + ')$');
// 50 is the longest alias length limit.
const ALIAS_LENGTH_LIMIT = 50;
// Regex used to match alias.
const AtSignMentionsRegexAliasRegex = new RegExp('(^|\\s|\\()(' + '[' + TRIGGERS + ']' + '((?:' + VALID_CHARS + '){0,' + ALIAS_LENGTH_LIMIT + '})' + ')$');
// At most, 5 suggestions are shown in the popup.
const SUGGESTION_LIST_LENGTH_LIMIT = 5;
function isTriggerVisibleInNearestScrollContainer(targetElement, containerElement) {
    const tRect = targetElement.getBoundingClientRect();
    const cRect = containerElement.getBoundingClientRect();
    return tRect.top > cRect.top && tRect.top < cRect.bottom;
}
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;
}
export function useDynamicPositioning(resolution, targetElement, onReposition, onVisibilityChange) {
    const [editor] = useLexicalComposerContext();
    useEffect(() => {
        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);
            return () => {
                resizeObserver.unobserve(targetElement);
                window.removeEventListener('resize', onReposition);
                document.removeEventListener('scroll', handleScroll, true);
            };
        }
    }, [targetElement, editor, onVisibilityChange, onReposition, resolution]);
}
export function useMenuAnchorRef(resolution, setResolution, className) {
    const [anchorElementRef, setAnchorElementRef] = useState(null);
    const [editor] = useLexicalComposerContext();
    //const anchorElementRef = useRef<HTMLElement>(null);
    const positionMenu = useCallback(() => {
        const rootElement = editor.getRootElement();
        const containerDiv = anchorElementRef;
        const menuEle = containerDiv.firstChild;
        if (rootElement !== null && resolution !== null) {
            const { left, top, width, height } = resolution.range.getBoundingClientRect();
            containerDiv.style.top = `${top + window.scrollY}px`;
            containerDiv.style.left = `${left + window.scrollX}px`;
            containerDiv.style.height = `${height}px`;
            containerDiv.style.width = `${width}px`;
            if (menuEle !== null) {
                const menuRect = menuEle.getBoundingClientRect();
                const menuHeight = menuRect.height;
                const menuWidth = menuRect.width;
                const rootElementRect = rootElement.getBoundingClientRect();
                if (left + menuWidth > rootElementRect.right) {
                    containerDiv.style.left = `${rootElementRect.right - menuWidth + window.scrollX}px`;
                }
                const margin = 10;
                if ((top + menuHeight > window.innerHeight || top + menuHeight > rootElementRect.bottom) && top - rootElementRect.top > menuHeight) {
                    containerDiv.style.top = `${top - menuHeight + window.scrollY - (height + margin)}px`;
                }
            }
            if (!containerDiv.isConnected) {
                if (className != null) {
                    containerDiv.className = className;
                }
                containerDiv.setAttribute('aria-label', 'Typeahead menu');
                containerDiv.setAttribute('id', 'typeahead-menu');
                containerDiv.setAttribute('role', 'listbox');
                containerDiv.style.display = 'block';
                containerDiv.style.position = 'absolute';
                document.body.append(containerDiv);
            }
            setAnchorElementRef(containerDiv);
            rootElement.setAttribute('aria-controls', 'typeahead-menu');
        }
    }, [anchorElementRef, editor, resolution, className]);
    useEffect(() => {
        if (resolution && !anchorElementRef) {
            setAnchorElementRef(document.createElement('div'));
        }
    }, [resolution]);
    useEffect(() => {
        const rootElement = editor.getRootElement();
        if (resolution !== null && anchorElementRef) {
            positionMenu();
            return () => {
                if (rootElement !== null) {
                    rootElement.removeAttribute('aria-controls');
                }
                const containerDiv = anchorElementRef;
                if (containerDiv !== null && containerDiv.isConnected) {
                    containerDiv.remove();
                }
            };
        }
    }, [anchorElementRef, editor, positionMenu, resolution]);
    const onVisibilityChange = useCallback((isInView) => {
        if (resolution !== null) {
            if (!isInView) {
                setResolution(null);
            }
        }
    }, [resolution, setResolution]);
    useDynamicPositioning(resolution, anchorElementRef, positionMenu, onVisibilityChange);
    return anchorElementRef;
}
const mentionsCache = new Map();
function useMentionLookupService(mentionString) {
    const [results, setResults] = useState(null);
    useEffect(() => {
        const cachedResults = mentionsCache.get(mentionString);
        if (cachedResults === null) {
            return;
        }
        else if (cachedResults !== undefined) {
            setResults(cachedResults);
            return;
        }
        mentionsCache.set(mentionString, null);
        http
            .request({
            url: Endpoints.UserSearch.url(),
            method: Endpoints.UserSearch.method,
            params: { user: mentionString, limit: 5 }
        })
            .then((res) => {
            mentionsCache.set(mentionString, res.data.results);
            setResults(res.data.results);
        });
    }, [mentionString]);
    return results;
}
function MentionsTypeaheadItem({ index, isHovered, isSelected, onClick, onMouseEnter, onMouseLeave, result }) {
    const liRef = useRef(null);
    return (_jsxs("li", Object.assign({ tabIndex: -1, className: classNames('item', { ['selected']: isSelected, ['hovered']: isHovered }), ref: liRef, role: "option", "aria-selected": isSelected, id: 'typeahead-item-' + index, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onClick: onClick }, { children: [_jsx(Avatar, { alt: result.username, src: result.avatar }), " ", result.username] }), result.id));
}
function MentionsTypeahead({ close, editor, resolution, className = '' }) {
    const divRef = useRef(null);
    const match = resolution.match;
    const results = useMentionLookupService(match.matchingString);
    const [selectedIndex, setSelectedIndex] = useState(null);
    const [hoveredIndex, setHoveredIndex] = useState(null);
    const applyCurrentSelected = useCallback((index) => {
        index = index || selectedIndex;
        if (results === null || index === null) {
            return;
        }
        const selectedEntry = results[index];
        close();
        createMentionNodeFromSearchResult(editor, selectedEntry, match);
    }, [close, match, editor, results, selectedIndex]);
    const updateSelectedIndex = useCallback((index) => {
        const rootElem = editor.getRootElement();
        if (rootElem !== null) {
            rootElem.setAttribute('aria-activedescendant', 'typeahead-item-' + index);
            setSelectedIndex(index);
        }
    }, [editor]);
    useEffect(() => {
        return () => {
            const rootElem = editor.getRootElement();
            if (rootElem !== null) {
                rootElem.removeAttribute('aria-activedescendant');
            }
        };
    }, [editor]);
    useIsomorphicLayoutEffect(() => {
        if (results === null) {
            setSelectedIndex(null);
        }
        else if (selectedIndex === null) {
            updateSelectedIndex(0);
        }
    }, [results, selectedIndex, updateSelectedIndex]);
    useEffect(() => {
        return mergeRegister(editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (payload) => {
            const event = payload;
            if (results !== null && selectedIndex !== null) {
                if (selectedIndex < SUGGESTION_LIST_LENGTH_LIMIT - 1 && selectedIndex !== results.length - 1) {
                    updateSelectedIndex(selectedIndex + 1);
                    event.preventDefault();
                    event.stopImmediatePropagation();
                }
            }
            return true;
        }, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ARROW_UP_COMMAND, (payload) => {
            const event = payload;
            if (results !== null && selectedIndex !== null) {
                if (selectedIndex !== 0) {
                    updateSelectedIndex(selectedIndex - 1);
                    event.preventDefault();
                    event.stopImmediatePropagation();
                }
            }
            return true;
        }, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ESCAPE_COMMAND, (payload) => {
            const event = payload;
            if (results === null || selectedIndex === null) {
                return false;
            }
            event.preventDefault();
            event.stopImmediatePropagation();
            close();
            return true;
        }, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_TAB_COMMAND, (payload) => {
            const event = payload;
            if (results === null || selectedIndex === null) {
                return false;
            }
            event.preventDefault();
            event.stopImmediatePropagation();
            applyCurrentSelected();
            return true;
        }, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
            if (results === null || selectedIndex === null) {
                return false;
            }
            if (event !== null) {
                event.preventDefault();
                event.stopImmediatePropagation();
            }
            applyCurrentSelected();
            return true;
        }, COMMAND_PRIORITY_LOW));
    }, [applyCurrentSelected, close, editor, results, selectedIndex, updateSelectedIndex]);
    if (results === null) {
        return null;
    }
    return (_jsx("div", Object.assign({ className: className, "aria-label": "Suggested mentions", ref: divRef, role: "listbox" }, { children: _jsx("ul", { children: results.slice(0, SUGGESTION_LIST_LENGTH_LIMIT).map((result, i) => (_jsx(MentionsTypeaheadItem, { index: i, isHovered: i === hoveredIndex, isSelected: i === selectedIndex, onClick: () => {
                    applyCurrentSelected(i);
                }, onMouseEnter: () => {
                    setHoveredIndex(i);
                }, onMouseLeave: () => {
                    setHoveredIndex(null);
                }, result: result }, result.id))) }) })));
}
function checkForCapitalizedNameMentions(text, minMatchLength) {
    const match = CapitalizedNameMentionsRegex.exec(text);
    if (match !== null) {
        // The strategy ignores leading whitespace but we need to know it's
        // length to add it to the leadOffset
        const maybeLeadingWhitespace = match[1];
        const matchingString = match[2];
        if (matchingString != null && matchingString.length >= minMatchLength) {
            return {
                leadOffset: match.index + maybeLeadingWhitespace.length,
                matchingString,
                replaceableString: matchingString
            };
        }
    }
    return null;
}
function checkForAtSignMentions(text, minMatchLength) {
    let match = AtSignMentionsRegex.exec(text);
    if (match === null) {
        match = AtSignMentionsRegexAliasRegex.exec(text);
    }
    if (match !== null) {
        // The strategy ignores leading whitespace but we need to know it's
        // length to add it to the leadOffset
        const maybeLeadingWhitespace = match[1];
        const matchingString = match[3];
        if (matchingString.length >= minMatchLength) {
            return {
                leadOffset: match.index + maybeLeadingWhitespace.length,
                matchingString,
                replaceableString: match[2]
            };
        }
    }
    return null;
}
function getPossibleMentionMatch(text) {
    const match = checkForAtSignMentions(text, 1);
    return match === null ? checkForCapitalizedNameMentions(text, 3) : match;
}
function getTextUpToAnchor(selection) {
    const anchor = selection.anchor;
    if (anchor.type !== 'text') {
        return null;
    }
    const anchorNode = anchor.getNode();
    // We should not be attempting to extract mentions out of nodes
    // that are already being used for other core things. This is
    // especially true for immutable nodes, which can't be mutated at all.
    if (!anchorNode.isSimpleText()) {
        return null;
    }
    const anchorOffset = anchor.offset;
    return anchorNode.getTextContent().slice(0, anchorOffset);
}
function tryToPositionRange(match, range) {
    const domSelection = window.getSelection();
    if (domSelection === null || !domSelection.isCollapsed) {
        return false;
    }
    const anchorNode = domSelection.anchorNode;
    const startOffset = match.leadOffset;
    const endOffset = domSelection.anchorOffset;
    if (anchorNode == null || endOffset == null) {
        return false;
    }
    try {
        range.setStart(anchorNode, startOffset);
        range.setEnd(anchorNode, endOffset);
    }
    catch (error) {
        return false;
    }
    return true;
}
function getMentionsTextToSearch(editor) {
    let text = null;
    editor.getEditorState().read(() => {
        const selection = $getSelection();
        if (!$isRangeSelection(selection)) {
            return;
        }
        text = getTextUpToAnchor(selection);
    });
    return text;
}
/**
 * Walk backwards along user input and forward through entity title to try
 * and replace more of the user's text with entity.
 *
 * E.g. User types "Hello Sarah Smit" and we match "Smit" to "Sarah Smith".
 * Replacing just the match would give us "Hello Sarah Sarah Smith".
 * Instead we find the string "Sarah Smit" and replace all of it.
 */
function getMentionOffset(documentText, entryText, offset) {
    let triggerOffset = offset;
    for (let ii = triggerOffset; ii <= entryText.length; ii++) {
        if (documentText.substr(-ii) === entryText.substr(0, ii)) {
            triggerOffset = ii;
        }
    }
    return triggerOffset;
}
/**
 * From a Typeahead Search Result, replace plain text from search offset and
 * render a newly created MentionNode.
 */
function createMentionNodeFromSearchResult(editor, user, match) {
    editor.update(() => {
        const selection = $getSelection();
        if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
            return;
        }
        const anchor = selection.anchor;
        if (anchor.type !== 'text') {
            return;
        }
        const anchorNode = anchor.getNode();
        // We should not be attempting to extract mentions out of nodes
        // that are already being used for other core things. This is
        // especially true for immutable nodes, which can't be mutated at all.
        if (!anchorNode.isSimpleText()) {
            return;
        }
        const selectionOffset = anchor.offset;
        const textContent = anchorNode.getTextContent().slice(0, selectionOffset);
        const characterOffset = match.replaceableString.length;
        // Given a known offset for the mention match, look backward in the
        // text to see if there's a longer match to replace.
        const mentionOffset = getMentionOffset(textContent, user.username, characterOffset);
        const startOffset = selectionOffset - mentionOffset;
        if (startOffset < 0) {
            return;
        }
        let nodeToReplace;
        if (startOffset === 0) {
            [nodeToReplace] = anchorNode.splitText(selectionOffset);
        }
        else {
            [, nodeToReplace] = anchorNode.splitText(startOffset, selectionOffset);
        }
        const mentionNode = createMentionNode(user);
        nodeToReplace.replace(mentionNode);
        mentionNode.select();
    });
}
function isSelectionOnEntityBoundary(editor, offset) {
    if (offset !== 0) {
        return false;
    }
    return editor.getEditorState().read(() => {
        const selection = $getSelection();
        if ($isRangeSelection(selection)) {
            const anchor = selection.anchor;
            const anchorNode = anchor.getNode();
            const prevSibling = anchorNode.getPreviousSibling();
            // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
            // @ts-ignore
            return $isTextNode(prevSibling) && prevSibling.isTextEntity();
        }
        return false;
    });
}
const classes = {
    root: `${PREFIX}-mention-plugin-root`
};
const Root = styled(MentionsTypeahead, {
    name: PREFIX,
    slot: 'MentionPluginRoot'
})(() => ({}));
function useMentions(editor, anchorClassName = null) {
    const [resolution, setResolution] = useState(null);
    const anchorElementRef = useMenuAnchorRef(resolution, setResolution, anchorClassName);
    useEffect(() => {
        if (!editor.hasNodes([MentionNode])) {
            throw new Error('MentionsPlugin: MentionNode not registered on editor');
        }
    }, [editor]);
    useEffect(() => {
        let activeRange = document.createRange();
        let previousText = null;
        const updateListener = ({ prevEditorState }) => {
            if (prevEditorState.isEmpty()) {
                return;
            }
            const range = activeRange;
            const text = getMentionsTextToSearch(editor);
            if (text === previousText || range === null) {
                return;
            }
            previousText = text;
            if (text === null) {
                return;
            }
            const match = getPossibleMentionMatch(text);
            if (match !== null && !isSelectionOnEntityBoundary(editor, match.leadOffset)) {
                const isRangePositioned = tryToPositionRange(match, range);
                if (isRangePositioned !== null) {
                    setResolution({
                        match,
                        range
                    });
                    return;
                }
            }
            setResolution(null);
        };
        const removeUpdateListener = editor.registerUpdateListener(updateListener);
        return () => {
            activeRange = null;
            removeUpdateListener();
        };
    }, [editor]);
    const closeTypeahead = useCallback(() => {
        setResolution(null);
    }, [resolution]);
    if (resolution === null || editor === null) {
        return null;
    }
    if (!anchorElementRef) {
        return null;
    }
    return (_jsx(ClickAwayListener, Object.assign({ onClickAway: closeTypeahead }, { children: _jsx(Portal, Object.assign({ container: anchorElementRef }, { children: _jsx(Root, { close: closeTypeahead, resolution: resolution, editor: editor, className: classes.root }) })) })));
}
export default function MentionsPlugin() {
    const [editor] = useLexicalComposerContext();
    return useMentions(editor);
}
