import { useCallback, useLayoutEffect, useRef, type Ref } from 'react'
import { useMergedRefs } from '@base-ui/utils/useMergedRefs'
import StyleObserver from 'style-observer'

import { styleScheduler } from '../utils/index.ts'
import { useEffectWithRefDeps } from './useEffectWithRefDeps.ts'

type Record<T extends string> = Parameters<ConstructorParameters<typeof StyleObserver>[0]>[0][0] & {
  property: T
}

/**
 * Hook that observes style changes on an element. Calls the `onChange` callback whenever specified
 * CSS properties change.
 */
export function useStyleObserver<T extends string>(
  properties: T[],
  onChange: (records: Record<T>[]) => void,
  deps: unknown[],
  options: {
    ref?: Ref<HTMLElement> | undefined
    immediate?: boolean | undefined
    disabled?: boolean | undefined
  } = {},
) {
  const { ref: passedRef, immediate = false, disabled = false } = options
  const elementRef = useRef<HTMLElement>(null)
  const watchedRef = useMergedRefs(elementRef, passedRef)

  // oxlint-disable-next-line react-hooks/exhaustive-deps
  const memoizedOnChange = useCallback(onChange, deps)

  const [callbackRef] = useEffectWithRefDeps(
    () => {
      const element = elementRef.current
      if (!element || properties.length === 0 || disabled) return

      // TODO: Use `style-observer`'s `immediate` option when it exists: https://github.com/LeaVerou/style-observer/issues/45
      if (immediate) {
        styleScheduler.scheduleRead(() => {
          const computedStyle = window.getComputedStyle(element)
          memoizedOnChange(
            properties.map((property) => ({
              target: element,
              property,
              value: computedStyle.getPropertyValue(property),
              oldValue: '',
            })),
          )
        })
      }

      const styleObserver = new StyleObserver((records) => {
        if (records.length === 0) return
        memoizedOnChange(records as Record<T>[])
      })

      styleObserver.observe(element, properties)

      return () => {
        styleObserver.unobserve(element, properties)
      }
    },
    [properties, memoizedOnChange, immediate, disabled],
    [watchedRef],
    {
      deepCompare: true,
      hook: useLayoutEffect,
    },
  )

  return callbackRef
}
