const ROOT_SELECTOR = '[data-testid="mnote-leptos-tiptap-island-editor-root"]';

export const activeEditorRootForSlashMenu = () => {
  const selection = window.getSelection();
  const anchorNode = selection?.anchorNode || null;
  const anchorElement = anchorNode && anchorNode.nodeType === Node.ELEMENT_NODE
    ? anchorNode
    : anchorNode?.parentElement || null;
  const roots = Array.from(document.querySelectorAll(ROOT_SELECTOR)).filter((node) => (
    node instanceof HTMLElement
      && node.offsetParent !== null
      && node.getAttribute('data-mnote-side-target-unsupported') !== 'true'
  ));
  const intendedRoot = window.__mnoteIntendedSlashRoot instanceof HTMLElement
    && roots.includes(window.__mnoteIntendedSlashRoot)
    ? window.__mnoteIntendedSlashRoot
    : null;
  if (intendedRoot) return intendedRoot;
  const focused = document.activeElement instanceof Element
    ? roots.find((root) => root.contains(document.activeElement))
    : null;
  if (focused) return focused;
  if (anchorElement instanceof Element) {
    const activeRoot = roots.find((root) => root.contains(anchorElement));
    if (activeRoot) return activeRoot;
  }
  return roots.find((root) => root.querySelector('.ProseMirror:focus-within')) || roots[0] || null;
};

export const markIntendedSlashRoot = (entry) => {
  const root = entry?.view?.runtimeDescriptor?.root;
  if (!(root instanceof HTMLElement) || !root.isConnected) return;
  window.__mnoteIntendedSlashRoot = root;
  hideSlashMenusOutsideRoot(root);
  scheduleSlashMenuPosition(root);
};

export const slashMenuAnchorFromSelection = (root) => {
  const currentBlockFromSelection = () => {
    try {
      const selection = window.getSelection();
      const anchorNode = selection?.anchorNode || null;
      const editor = root.querySelector('.ProseMirror');
      const anchorElement = anchorNode && anchorNode.nodeType === Node.ELEMENT_NODE
        ? anchorNode
        : anchorNode?.parentElement || null;
      if (!(editor instanceof HTMLElement) || !(anchorElement instanceof Element) || !editor.contains(anchorElement)) return null;
      let current = anchorElement;
      while (current && current.parentElement && current.parentElement !== editor) {
        current = current.parentElement;
      }
      return current instanceof HTMLElement && current.parentElement === editor ? current : null;
    } catch (_) {
      return null;
    }
  };
  const fallback = () => {
    const editor = root.querySelector('.ProseMirror');
    if (editor instanceof HTMLElement) {
      const block = currentBlockFromSelection()
        || Array.from(editor.children).find((node) => node instanceof HTMLElement && node.matches(':focus-within, .ProseMirror-selectednode'))
        || Array.from(editor.children).find((node) => node instanceof HTMLElement && node.getBoundingClientRect().height > 0);
      if (block instanceof HTMLElement) {
        const rect = block.getBoundingClientRect();
        return { top: rect.bottom + 8, left: rect.left, anchorTop: rect.top };
      }
    }
    const rect = root.getBoundingClientRect();
    return { top: rect.top + 24, left: rect.left + 24, anchorTop: rect.top + 24 };
  };
  try {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount <= 0) return fallback();
    const anchorNode = selection.anchorNode;
    if (anchorNode && !root.contains(anchorNode.nodeType === Node.ELEMENT_NODE ? anchorNode : anchorNode.parentElement)) {
      return fallback();
    }
    const rect = selection.getRangeAt(0).getBoundingClientRect();
    if (rect && (rect.top > 0 || rect.left > 0 || rect.height > 0)) {
      return { top: rect.bottom + 8, left: rect.left, anchorTop: rect.top };
    }
  } catch (_) {}
  return fallback();
};

export const setSlashMenuInactive = (menu, inactive) => {
  if (!(menu instanceof HTMLElement)) return;
  if (inactive) {
    if (menu.getAttribute('data-mnote-slash-inactive') !== 'true') {
      menu.setAttribute('data-mnote-slash-inactive', 'true');
    }
    if (menu.style.display !== 'none') menu.style.display = 'none';
    return;
  }
  if (menu.getAttribute('data-mnote-slash-inactive') === 'true') {
    menu.removeAttribute('data-mnote-slash-inactive');
  }
  if (menu.style.display === 'none') menu.style.display = '';
};

export const setSlashMenuStyle = (menu, property, value) => {
  if (!(menu instanceof HTMLElement)) return;
  if (menu.style[property] !== value) menu.style[property] = value;
};

export const setSlashMenuAttribute = (menu, name, value) => {
  if (!(menu instanceof HTMLElement)) return;
  if (menu.getAttribute(name) !== value) menu.setAttribute(name, value);
};

export const hideSlashMenusOutsideRoot = (activeRoot) => {
  document.querySelectorAll(`${ROOT_SELECTOR} [data-testid="mnote-leptos-tiptap-slash-menu"]`).forEach((menu) => {
    const root = menu.closest(ROOT_SELECTOR);
    if (root !== activeRoot) setSlashMenuInactive(menu, true);
  });
};

export const positionSlashMenuForRoot = (root) => {
  const activeRoot = activeEditorRootForSlashMenu();
  if (!(root instanceof HTMLElement)) root = activeRoot;
  if (!(root instanceof HTMLElement)) return;
  if (activeRoot instanceof HTMLElement && root !== activeRoot) {
    const inactiveMenu = root.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]');
    setSlashMenuInactive(inactiveMenu, true);
    hideSlashMenusOutsideRoot(activeRoot);
    return;
  }
  hideSlashMenusOutsideRoot(root);
  const menu = root.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]');
  if (!(menu instanceof HTMLElement)) return;
  setSlashMenuInactive(menu, false);
  const anchor = slashMenuAnchorFromSelection(root);
  const gap = 8;
  const menuWidth = Math.min(316, Math.max(160, window.innerWidth - 16));
  const menuHeight = Math.min(430, Math.max(120, window.innerHeight - 16));
  const left = Math.min(Math.max(gap, anchor.left), Math.max(gap, window.innerWidth - menuWidth - gap));
  const belowTop = Math.min(Math.max(gap, anchor.top), Math.max(gap, window.innerHeight - 120));
  let top = belowTop + menuHeight > window.innerHeight - gap
    ? Math.min(Math.max(gap, anchor.anchorTop - menuHeight - gap), Math.max(gap, window.innerHeight - 120))
    : belowTop;
  const mindmap = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
  if (mindmap instanceof HTMLElement) {
    const mindmapRect = mindmap.getBoundingClientRect();
    const overlapsMindmap = top < mindmapRect.bottom && top + menuHeight > mindmapRect.top;
    if (overlapsMindmap && mindmapRect.bottom > window.innerHeight - 48) {
      top = Math.max(top, Math.max(gap, window.innerHeight - menuHeight - gap));
    } else if (overlapsMindmap && mindmapRect.bottom + menuHeight + gap <= window.innerHeight) {
      top = mindmapRect.bottom + gap;
    }
  }
  setSlashMenuStyle(menu, 'position', 'fixed');
  setSlashMenuStyle(menu, 'zIndex', '130');
  setSlashMenuStyle(menu, 'left', `${Math.round(left)}px`);
  setSlashMenuStyle(menu, 'top', `${Math.round(top)}px`);
  setSlashMenuStyle(menu, 'width', `min(316px, calc(100vw - 16px))`);
  setSlashMenuStyle(menu, 'maxHeight', `min(430px, calc(100vh - 16px))`);
  setSlashMenuAttribute(menu, 'data-mnote-slash-positioned', 'host');
};

export let pendingSlashMenuPositionFrame = 0;
export let pendingSlashMenuPositionRoot = null;
export const scheduleSlashMenuPosition = (root) => {
  if (root instanceof HTMLElement) pendingSlashMenuPositionRoot = root;
  if (pendingSlashMenuPositionFrame) return;
  pendingSlashMenuPositionFrame = window.requestAnimationFrame(() => {
    const targetRoot = pendingSlashMenuPositionRoot;
    pendingSlashMenuPositionFrame = 0;
    pendingSlashMenuPositionRoot = null;
    positionSlashMenuForRoot(targetRoot);
  });
};

export const shouldReactToSlashMenuMutation = (records, root) => {
  if (!Array.isArray(records) || !(root instanceof HTMLElement)) return false;
  return records.some((record) => {
    const target = record && record.target instanceof HTMLElement ? record.target : null;
    if (!target || !root.contains(target) && target !== root) return false;
    if (record.type === 'attributes') {
      return record.attributeName !== 'style' && record.attributeName !== 'data-mnote-slash-positioned' && record.attributeName !== 'data-mnote-slash-inactive';
    }
    if (record.type === 'childList') {
      return Array.from(record.addedNodes || []).some((node) => node instanceof HTMLElement && node.closest('[data-testid="mnote-leptos-tiptap-slash-menu"]'))
        || Array.from(record.removedNodes || []).some((node) => node instanceof HTMLElement && node.closest('[data-testid="mnote-leptos-tiptap-slash-menu"]'));
    }
    return true;
  });
};

export const scheduleGlobalSlashMenuPosition = () => {
  scheduleSlashMenuPosition(activeEditorRootForSlashMenu());
};

export const installGlobalSlashMenuPositioning = () => {
  if (window.__MNOTE_SLASH_MENU_POSITIONING_INSTALLED__) return;
  window.__MNOTE_SLASH_MENU_POSITIONING_INSTALLED__ = true;
  if (typeof MutationObserver === 'function' && document.body instanceof HTMLElement) {
    const observer = new MutationObserver(() => scheduleGlobalSlashMenuPosition());
    observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden'] });
  }
  document.addEventListener('keydown', (event) => {
    if (event.key === '/') scheduleGlobalSlashMenuPosition();
  }, true);
  document.addEventListener('selectionchange', () => scheduleGlobalSlashMenuPosition(), true);
  window.addEventListener('resize', scheduleGlobalSlashMenuPosition);
  window.addEventListener('scroll', scheduleGlobalSlashMenuPosition, true);
};

export const observeSlashMenuPosition = (view) => {
  const root = view.runtimeDescriptor.root;
  if (!(root instanceof HTMLElement) || typeof MutationObserver !== 'function') return;
  const observer = new MutationObserver((records) => {
    if (!view.disposed && shouldReactToSlashMenuMutation(records, root)) {
      scheduleSlashMenuPosition(root);
    }
  });
  observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'hidden', 'class', 'data-mnote-slash-positioned', 'data-mnote-slash-inactive'] });
  view.disconnectSlashObserver = () => observer.disconnect();
};
