import { useCallback, useEffect, useRef, useState } from "react"
import type { ComponentProps } from "react"
import { createPortal } from "react-dom"
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
import { cn } from "@workspace/ui/lib/utils"

import { LexicalActionTree } from "./actions-view"
import { useLexicalActions } from "./actions-context"
import { LexicalActionRuntimeProvider } from "./toolbar"

type BubblePosition = {
  left: number
  top: number
  placement: "above" | "below"
}

export type LexicalBubbleToolbarProps = ComponentProps<"div">

export function LexicalBubbleToolbar({
  className,
  children,
  ...props
}: LexicalBubbleToolbarProps) {
  const actions = useLexicalActions("bubble")

  if (actions.length === 0 && children == null) return null

  return (
    <LexicalBubbleToolbarContent
      actions={actions}
      className={className}
      {...props}
    >
      {children}
    </LexicalBubbleToolbarContent>
  )
}

function LexicalBubbleToolbarContent({
  actions,
  className,
  children,
  ...props
}: LexicalBubbleToolbarProps & {
  actions: ReturnType<typeof useLexicalActions>
}) {
  const [editor] = useLexicalComposerContext()
  const [position, setPosition] = useState<BubblePosition | null>(null)
  const frameRef = useRef<number | null>(null)

  const updatePosition = useCallback(() => {
    if (frameRef.current !== null) {
      cancelAnimationFrame(frameRef.current)
    }

    frameRef.current = requestAnimationFrame(() => {
      frameRef.current = null
      const rootElement = editor.getRootElement()
      const selection = window.getSelection()

      if (
        !rootElement ||
        !selection ||
        selection.rangeCount === 0 ||
        selection.isCollapsed ||
        !selection.anchorNode ||
        !rootElement.contains(selection.anchorNode)
      ) {
        setPosition(null)
        return
      }

      const range = selection.getRangeAt(0)
      if (typeof range.getBoundingClientRect !== "function") {
        setPosition(null)
        return
      }

      const rect = range.getBoundingClientRect()
      if (rect.width === 0 && rect.height === 0) {
        setPosition(null)
        return
      }

      const placement = rect.top >= 72 ? "above" : "below"
      setPosition({
        left: Math.min(
          Math.max(rect.left + rect.width / 2, 24),
          window.innerWidth - 24
        ),
        top: placement === "above" ? rect.top - 8 : rect.bottom + 8,
        placement,
      })
    })
  }, [editor])

  useEffect(() => {
    const unregisterUpdate = editor.registerUpdateListener(updatePosition)
    document.addEventListener("selectionchange", updatePosition)
    window.addEventListener("resize", updatePosition)
    window.addEventListener("scroll", updatePosition, true)

    return () => {
      unregisterUpdate()
      document.removeEventListener("selectionchange", updatePosition)
      window.removeEventListener("resize", updatePosition)
      window.removeEventListener("scroll", updatePosition, true)
      if (frameRef.current !== null) {
        cancelAnimationFrame(frameRef.current)
      }
    }
  }, [editor, updatePosition])

  if (!position || typeof document === "undefined") return null

  return createPortal(
    <LexicalActionRuntimeProvider>
      <div
        role="toolbar"
        className={cn(
          "fixed z-50 flex min-h-11 -translate-x-1/2 items-center gap-1 rounded-xl bg-popover p-1.5 text-popover-foreground shadow-xl ring-1 ring-foreground/5",
          position.placement === "above"
            ? "-translate-y-full"
            : "translate-y-0",
          className
        )}
        style={{ left: position.left, top: position.top }}
        {...props}
      >
        <LexicalActionTree items={actions} />
        {children}
      </div>
    </LexicalActionRuntimeProvider>,
    document.body
  )
}
