// https://github.com/ZeeCoder/use-resize-observer
import { useEffect, useRef } from "react"

type SubscriberResponse = (() => void) | void

function useElement<T extends Element>(
  fn: (element: T) => SubscriberResponse,
  element: T | null,
) {
  const lastReportRef = useRef<{
    element: T | null
    subscriber: typeof fn
    cleanup?: SubscriberResponse
  } | null>(null)

  if (
    lastReportRef.current?.element === element &&
    lastReportRef.current.subscriber === fn
  ) {
    // noop
  } else {
    lastReportRef.current?.cleanup?.()
    lastReportRef.current = {
      element,
      subscriber: fn,
      cleanup: element ? fn(element) : undefined,
    }
  }

  useEffect(
    () => () => {
      if (lastReportRef.current?.cleanup) {
        lastReportRef.current.cleanup()
        lastReportRef.current = null
      }
    },
    [],
  )
}

type ObservedSize = {
  width: number | undefined
  height: number | undefined
}

type ResizeHandler = (size: ObservedSize) => void

const { round } = Math

function useResizeObserver<T extends Element>(opts: {
  node: T | null
  onResize: ResizeHandler
}) {
  const { onResize } = opts

  const onResizeRef = useRef<ResizeHandler | undefined>(undefined)
  onResizeRef.current = onResize

  // Using a ref to track the previous width / height to avoid unnecessary renders.
  const previous = useRef<{ width?: number; height?: number }>({})

  useElement<T>(element => {
    const obs = new ResizeObserver(entries => {
      const [entry] = entries

      const reportedWidth = entry.contentBoxSize[0].inlineSize
      const reportedHeight = entry.contentBoxSize[0].blockSize

      const newWidth = reportedWidth ? round(reportedWidth) : undefined
      const newHeight = reportedHeight ? round(reportedHeight) : undefined

      if (previous.current.width !== newWidth || previous.current.height !== newHeight) {
        const newSize = { width: newWidth, height: newHeight }
        previous.current.width = newWidth
        previous.current.height = newHeight
        onResizeRef.current?.(newSize)
      }
    })

    obs.observe(element, { box: undefined })

    return () => {
      obs.unobserve(element)
    }
  }, opts.node)
}

export default useResizeObserver
