/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { createCommand, COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND, KEY_ENTER_COMMAND, $getSelection, $isRangeSelection, $getNodeByKey } from 'lexical';
import React, { useLayoutEffect, useEffect, useRef, useCallback, useState, useMemo } from 'react';
import { mergeRegister } from '@lexical/utils';
import ReactDOM from 'react-dom';
import { jsx, jsxs } from 'react/jsx-runtime';

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */


// Webpack + React 17 fails to compile on the usage of `React.startTransition` or
// `React["startTransition"]` even if it's behind a feature detection of
// `"startTransition" in React`. Moving this to a constant avoids the issue :/
const START_TRANSITION = 'startTransition';
function startTransition(callback) {
  if (START_TRANSITION in React) {
    React[START_TRANSITION](callback);
  } else {
    callback();
  }
}

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined';

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */


// This workaround is no longer necessary in React 19,
// but we currently support React >=17.x
// https://github.com/facebook/react/pull/26395
const useLayoutEffectImpl = CAN_USE_DOM ? useLayoutEffect : useEffect;

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

class MenuOption {
  key;
  ref;
  icon;
  title;
  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
function getScrollParent(element, includeHidden) {
  let style = getComputedStyle(element);
  const excludeStaticParent = style.position === 'absolute';
  const overflowRegex = /(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();
  const VISIBILITY_MARGIN_PX = 6;
  return tRect.top >= cRect.top - VISIBILITY_MARGIN_PX && tRect.top <= cRect.bottom + VISIBILITY_MARGIN_PX;
}

// Reposition the menu on scroll, window resize, and element resize.
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) : 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]);
}
const SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND = createCommand('SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND');
function MenuItem({
  index,
  isSelected,
  onClick,
  onMouseEnter,
  option
}) {
  let className = 'item';
  if (isSelected) {
    className += ' selected';
  }
  return /*#__PURE__*/jsxs("li", {
    tabIndex: -1,
    className: className,
    ref: option.setRefElement,
    role: "option",
    "aria-selected": isSelected,
    id: 'typeahead-item-' + index,
    onMouseEnter: onMouseEnter,
    onClick: onClick,
    children: [option.icon, /*#__PURE__*/jsx("span", {
      className: "text",
      children: option.title
    })]
  }, option.key);
}
function LexicalMenu({
  close,
  editor,
  anchorElementRef,
  resolution,
  options,
  menuRenderFn: menuRenderFnProp,
  onSelectOption,
  shouldSplitNodeWithQuery = false,
  commandPriority = COMMAND_PRIORITY_LOW,
  preselectFirstItem = true
}) {
  const [rawSelectedIndex, setHighlightedIndex] = useState(null);
  // Clamp highlighted index if options list shrinks
  const selectedIndex = rawSelectedIndex !== null ? Math.min(options.length - 1, rawSelectedIndex) : null;
  const matchingString = resolution.match && resolution.match.matchingString;
  useEffect(() => {
    if (preselectFirstItem) {
      setHighlightedIndex(0);
    }
  }, [matchingString, preselectFirstItem]);
  const selectOptionAndCleanUp = useCallback(selectedEntry => {
    editor.update(() => {
      const textNodeContainingQuery = resolution.match != null && shouldSplitNodeWithQuery ? $splitNodeContainingQuery(resolution.match) : null;
      onSelectOption(selectedEntry, textNodeContainingQuery, close, resolution.match ? resolution.match.matchingString : '');
    });
  }, [editor, shouldSplitNodeWithQuery, resolution.match, onSelectOption, close]);
  const updateSelectedIndex = useCallback(index => {
    const rootElem = editor.getRootElement();
    if (rootElem !== null) {
      rootElem.setAttribute('aria-activedescendant', 'typeahead-item-' + index);
      setHighlightedIndex(index);
    }
  }, [editor]);
  const defaultMenuRenderFn = useCallback(() => {
    return anchorElementRef.current && options.length ? /*#__PURE__*/ReactDOM.createPortal(/*#__PURE__*/jsx("div", {
      className: "typeahead-popover mentions-menu",
      children: /*#__PURE__*/jsx("ul", {
        children: options.map((option, i) => /*#__PURE__*/jsx(MenuItem, {
          index: i,
          isSelected: selectedIndex === i,
          onClick: () => {
            setHighlightedIndex(i);
            selectOptionAndCleanUp(option);
          },
          onMouseEnter: () => {
            setHighlightedIndex(i);
          },
          option: option
        }, option.key))
      })
    }), anchorElementRef.current) : null;
  }, [anchorElementRef, options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex]);
  useEffect(() => {
    return () => {
      const rootElem = editor.getRootElement();
      if (rootElem !== null) {
        rootElem.removeAttribute('aria-activedescendant');
      }
    };
  }, [editor]);
  useLayoutEffectImpl(() => {
    if (options === null) {
      setHighlightedIndex(null);
    } else if (selectedIndex === null && preselectFirstItem) {
      updateSelectedIndex(0);
    }
  }, [options, selectedIndex, updateSelectedIndex, preselectFirstItem]);
  useEffect(() => {
    return mergeRegister(editor.registerCommand(SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND, ({
      option
    }) => {
      if (option.ref && option.ref.current != null) {
        scrollIntoViewIfNeeded(option.ref.current);
        return true;
      }
      return false;
    }, commandPriority));
  }, [editor, updateSelectedIndex, commandPriority]);
  useEffect(() => {
    return mergeRegister(editor.registerCommand(KEY_ARROW_DOWN_COMMAND, payload => {
      const event = payload;
      if (options !== null && options.length) {
        const newSelectedIndex = selectedIndex === null ? 0 : selectedIndex !== options.length - 1 ? selectedIndex + 1 : 0;
        updateSelectedIndex(newSelectedIndex);
        const option = options[newSelectedIndex];
        if (!option) {
          updateSelectedIndex(-1);
          event.preventDefault();
          event.stopImmediatePropagation();
          return true;
        }
        if (option.ref && option.ref.current) {
          editor.dispatchCommand(SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND, {
            index: newSelectedIndex,
            option
          });
        }
        event.preventDefault();
        event.stopImmediatePropagation();
      }
      return true;
    }, commandPriority), editor.registerCommand(KEY_ARROW_UP_COMMAND, payload => {
      const event = payload;
      if (options !== null && options.length) {
        const newSelectedIndex = selectedIndex === null ? options.length - 1 : selectedIndex !== 0 ? selectedIndex - 1 : options.length - 1;
        updateSelectedIndex(newSelectedIndex);
        const option = options[newSelectedIndex];
        if (!option) {
          updateSelectedIndex(-1);
          event.preventDefault();
          event.stopImmediatePropagation();
          return true;
        }
        if (option.ref && option.ref.current) {
          scrollIntoViewIfNeeded(option.ref.current);
        }
        event.preventDefault();
        event.stopImmediatePropagation();
      }
      return true;
    }, commandPriority), editor.registerCommand(KEY_ESCAPE_COMMAND, payload => {
      const event = payload;
      event.preventDefault();
      event.stopImmediatePropagation();
      close();
      return true;
    }, commandPriority), editor.registerCommand(KEY_TAB_COMMAND, payload => {
      const event = payload;
      if (options === null || selectedIndex === null || options[selectedIndex] == null) {
        return false;
      }
      event.preventDefault();
      event.stopImmediatePropagation();
      selectOptionAndCleanUp(options[selectedIndex]);
      return true;
    }, commandPriority), editor.registerCommand(KEY_ENTER_COMMAND, event => {
      if (options === null || selectedIndex === null || options[selectedIndex] == null) {
        return false;
      }
      if (event !== null) {
        event.preventDefault();
        event.stopImmediatePropagation();
      }
      selectOptionAndCleanUp(options[selectedIndex]);
      return true;
    }, commandPriority));
  }, [selectOptionAndCleanUp, close, editor, options, selectedIndex, updateSelectedIndex, commandPriority]);
  const listItemProps = useMemo(() => ({
    options,
    selectOptionAndCleanUp,
    selectedIndex,
    setHighlightedIndex
  }), [selectOptionAndCleanUp, selectedIndex, options]);
  if (menuRenderFnProp != null) {
    return menuRenderFnProp(anchorElementRef, listItemProps, resolution.match ? resolution.match.matchingString : '');
  }
  return defaultMenuRenderFn();
}
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';
}
function useMenuAnchorRef(resolution, setResolution, className, parent = CAN_USE_DOM ? document.body : undefined, shouldIncludePageYOffset__EXPERIMENTAL = true) {
  const [editor] = useLexicalComposerContext();
  const initialAnchorElement = CAN_USE_DOM ? document.createElement('div') : null;
  const anchorElementRef = useRef(initialAnchorElement);
  const positionMenu = useCallback(() => {
    if (anchorElementRef.current === null || parent === undefined) {
      return;
    }
    anchorElementRef.current.style.top = anchorElementRef.current.style.bottom;
    const rootElement = editor.getRootElement();
    const containerDiv = anchorElementRef.current;
    const menuEle = containerDiv.firstChild;
    if (rootElement !== null && resolution !== null) {
      const {
        left,
        top,
        width,
        height
      } = resolution.getRect();
      const anchorHeight = anchorElementRef.current.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 = `${rootElementRect.right - 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');
      rootElement.setAttribute('aria-controls', 'typeahead-menu');
    }
  }, [editor, resolution, shouldIncludePageYOffset__EXPERIMENTAL, className, parent]);
  useEffect(() => {
    const rootElement = editor.getRootElement();
    if (resolution !== null) {
      positionMenu();
    }
    return () => {
      if (rootElement !== null) {
        rootElement.removeAttribute('aria-controls');
      }
      // eslint-disable-next-line react-hooks/exhaustive-deps
      const containerDiv = anchorElementRef.current;
      if (containerDiv !== null && containerDiv.isConnected) {
        containerDiv.remove();
        containerDiv.removeAttribute('id');
      }
    };
  }, [editor, positionMenu, resolution]);
  const onVisibilityChange = useCallback(isInView => {
    if (resolution !== null) {
      if (!isInView) {
        setResolution(null);
      }
    }
  }, [resolution, setResolution]);
  useDynamicPositioning(resolution, anchorElementRef.current, positionMenu, onVisibilityChange);

  // Append the context for the menu immediately
  if (initialAnchorElement != null && initialAnchorElement === anchorElementRef.current) {
    setContainerDivAttributes(initialAnchorElement, className);
    if (parent != null) {
      parent.append(initialAnchorElement);
    }
  }
  return anchorElementRef;
}

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

function LexicalNodeMenuPlugin({
  options,
  nodeKey,
  onClose,
  onOpen,
  onSelectOption,
  menuRenderFn,
  anchorClassName,
  commandPriority = COMMAND_PRIORITY_LOW,
  parent
}) {
  const [editor] = useLexicalComposerContext();
  const [resolution, setResolution] = useState(null);
  const anchorElementRef = useMenuAnchorRef(resolution, setResolution, anchorClassName, parent);
  const closeNodeMenu = useCallback(() => {
    setResolution(null);
    if (onClose != null && resolution !== null) {
      onClose();
    }
  }, [onClose, resolution]);
  const openNodeMenu = useCallback(res => {
    setResolution(res);
    if (onOpen != null && resolution === null) {
      onOpen(res);
    }
  }, [onOpen, resolution]);
  const positionOrCloseMenu = useCallback(() => {
    if (nodeKey) {
      editor.update(() => {
        const node = $getNodeByKey(nodeKey);
        const domElement = editor.getElementByKey(nodeKey);
        if (node != null && domElement != null) {
          if (resolution == null) {
            startTransition(() => openNodeMenu({
              getRect: () => domElement.getBoundingClientRect()
            }));
          }
        }
      });
    } else if (nodeKey == null && resolution != null) {
      closeNodeMenu();
    }
  }, [closeNodeMenu, editor, nodeKey, openNodeMenu, resolution]);
  useEffect(() => {
    positionOrCloseMenu();
  }, [positionOrCloseMenu, nodeKey]);
  useEffect(() => {
    if (nodeKey != null) {
      return editor.registerUpdateListener(({
        dirtyElements
      }) => {
        if (dirtyElements.get(nodeKey)) {
          positionOrCloseMenu();
        }
      });
    }
  }, [editor, positionOrCloseMenu, nodeKey]);
  return anchorElementRef.current === null || resolution === null || editor === null ? null : /*#__PURE__*/jsx(LexicalMenu, {
    close: closeNodeMenu,
    resolution: resolution,
    editor: editor,
    anchorElementRef: anchorElementRef,
    options: options,
    menuRenderFn: menuRenderFn,
    onSelectOption: onSelectOption,
    commandPriority: commandPriority
  });
}

export { LexicalNodeMenuPlugin, MenuOption };
