import { useLayoutEffect } from 'react'
import {
  blockElementById,
  editorBlockElement,
  type TolariaBlockNoteEditor,
} from './tolariaBlockNoteDom'
import type { SideMenuBlock } from './tolariaSideMenuBlocks'

type SideMenuAlignmentState = {
  attemptsRemaining: number
  frame: number | null
  hasObservedTargets: boolean
}
type SideMenuAlignmentContext = {
  blockId: string
  editorElement: HTMLElement
  observeTargets: () => void
  ownerWindow: Window
  retry: () => void
  state: SideMenuAlignmentState
}

const SIDE_MENU_ALIGNMENT_ATTEMPTS = 8

function sideMenuElementForEditor(editorElement: HTMLElement): HTMLElement | null {
  const container = editorElement.closest('.editor__blocknote-container') ?? editorElement
  const sideMenu = container.querySelector('.bn-side-menu')
  return sideMenu instanceof HTMLElement ? sideMenu : null
}

function blockTextAnchorRect(blockElement: HTMLElement): DOMRect | null {
  const content = blockElement.querySelector('.bn-block-content')
  const inlineContent = content?.querySelector('.bn-inline-content') ?? content
  if (!(inlineContent instanceof HTMLElement)) return null

  const ownerDocument = inlineContent.ownerDocument
  const range = ownerDocument.createRange()
  range.selectNodeContents(inlineContent)
  const firstLineRect = Array.from(range.getClientRects())
    .find((rect) => rect.width > 0 && rect.height > 0)
  const textRect = firstLineRect ?? range.getBoundingClientRect()
  range.detach()

  if (textRect.height > 0) return textRect

  const fallbackRect = inlineContent.getBoundingClientRect()
  return fallbackRect.height > 0 ? fallbackRect : null
}

function alignSideMenuWithBlockText(editorElement: HTMLElement, blockId: string): boolean {
  const blockElement = blockElementById(editorElement, blockId)
  const sideMenu = sideMenuElementForEditor(editorElement)
  if (!blockElement || !sideMenu) return false

  const anchorRect = blockTextAnchorRect(blockElement)
  if (!anchorRect) return false

  sideMenu.style.removeProperty('translate')
  const sideMenuRect = sideMenu.getBoundingClientRect()
  if (sideMenuRect.height <= 0) return false

  const anchorCenter = anchorRect.top + anchorRect.height / 2
  const sideMenuCenter = sideMenuRect.top + sideMenuRect.height / 2
  sideMenu.style.setProperty('translate', `0 ${anchorCenter - sideMenuCenter}px`)
  return true
}

function createSideMenuAlignmentState(): SideMenuAlignmentState {
  return {
    attemptsRemaining: SIDE_MENU_ALIGNMENT_ATTEMPTS,
    frame: null,
    hasObservedTargets: false,
  }
}

function createSideMenuResizeObserver(onResize: () => void): ResizeObserver | null {
  return typeof ResizeObserver === 'undefined'
    ? null
    : new ResizeObserver(onResize)
}

function observeSideMenuAlignmentTargets({
  blockId,
  editorElement,
  resizeObserver,
  state,
}: {
  blockId: string
  editorElement: HTMLElement
  resizeObserver: ResizeObserver | null
  state: SideMenuAlignmentState
}) {
  if (state.hasObservedTargets) return

  const blockElement = blockElementById(editorElement, blockId)
  const sideMenu = sideMenuElementForEditor(editorElement)
  if (!resizeObserver || !blockElement || !sideMenu) return

  resizeObserver.observe(blockElement)
  resizeObserver.observe(sideMenu)
  state.hasObservedTargets = true
}

function scheduleSideMenuTextAlignment(context: SideMenuAlignmentContext) {
  const { blockId, editorElement, observeTargets, ownerWindow, retry, state } = context
  if (state.frame !== null) return

  state.frame = ownerWindow.requestAnimationFrame(() => {
    state.frame = null
    const aligned = alignSideMenuWithBlockText(editorElement, blockId)
    observeTargets()
    if (!aligned && state.attemptsRemaining > 0) {
      state.attemptsRemaining -= 1
      retry()
    }
  })
}

function createSideMenuAlignmentCleanup({
  editorElement,
  ownerWindow,
  resizeObserver,
  scheduleAlignment,
  state,
}: {
  editorElement: HTMLElement
  ownerWindow: Window
  resizeObserver: ResizeObserver | null
  scheduleAlignment: () => void
  state: SideMenuAlignmentState
}) {
  return () => {
    if (state.frame !== null) ownerWindow.cancelAnimationFrame(state.frame)
    resizeObserver?.disconnect()
    ownerWindow.removeEventListener('resize', scheduleAlignment)
    sideMenuElementForEditor(editorElement)?.style.removeProperty('translate')
  }
}

function createSideMenuAlignmentController(editor: TolariaBlockNoteEditor, blockId: string) {
  const editorElement = editorBlockElement(editor)
  const ownerWindow = editorElement?.ownerDocument.defaultView
  if (!editorElement || !ownerWindow) return undefined

  const state = createSideMenuAlignmentState()
  let resizeObserver: ResizeObserver | null = null
  const observeTargets = () => observeSideMenuAlignmentTargets({
    blockId,
    editorElement,
    resizeObserver,
    state,
  })
  const scheduleAlignment = () => scheduleSideMenuTextAlignment({
    blockId,
    editorElement,
    observeTargets,
    ownerWindow,
    retry: scheduleAlignment,
    state,
  })

  resizeObserver = createSideMenuResizeObserver(scheduleAlignment)
  scheduleAlignment()
  observeTargets()
  ownerWindow.addEventListener('resize', scheduleAlignment)

  return createSideMenuAlignmentCleanup({
    editorElement,
    ownerWindow,
    resizeObserver,
    scheduleAlignment,
    state,
  })
}

export function useSideMenuTextAlignment(
  editor: TolariaBlockNoteEditor,
  block: SideMenuBlock | undefined,
) {
  const blockId = block?.id

  useLayoutEffect(() => {
    if (!blockId) return

    return createSideMenuAlignmentController(editor, blockId)
  }, [blockId, editor])
}
