import { DebouncedFunc, throttle } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';

interface ClientBox {
  top: number;
  right: number;
  bottom: number;
  left: number;
  width: number;
  height: number;
  x: number;
  y: number;
}

interface UseClientBoxOptions {
  enabled?: boolean;
  throttleMs?: number;
  scrollEl?: Element | Window | null;
}

type ClientBoxReturn = [
  ref: (node?: Element | null) => void,
  box: ClientBox | null,
  scrollRef: (node?: Window | Element | null) => void,
] & {
  ref: (node?: Element | null) => void;
  scrollRef: (node?: Window | Element | null) => void;
  box: ClientBox | null;
};

export function useClientBox(options: UseClientBoxOptions = {}) {
  const {
    enabled = true,
    throttleMs,
    scrollEl = typeof window === 'undefined' ? null : window,
  } = options;

  const [box, setBox] = useState<ClientBox | null>(null);
  const [ref, setRef] = useState<Element | null>(null);
  const [scrollRef, setScrollRef] = useState<Window | Element | null>(scrollEl);

  useEffect(() => {
    setScrollRef(scrollEl);
  }, [scrollEl]);

  // Function to measure the box
  const updateBox = useCallback(() => {
    if (!ref) return;

    const rect = ref.getBoundingClientRect();
    setBox({
      top: rect.top,
      right: rect.right,
      bottom: rect.bottom,
      left: rect.left,
      width: rect.width,
      height: rect.height,
      x: rect.x,
      y: rect.y,
    });
  }, [ref]);

  // Measure function, optionally throttled
  const measure = useMemo<DebouncedFunc<() => void> | (() => void)>(
    () =>
      typeof throttleMs === 'number'
        ? throttle(updateBox, throttleMs || 0)
        : updateBox,
    [updateBox, throttleMs]
  );

  // Listen to scrolling
  useEffect(() => {
    if (!ref || !scrollRef || !enabled) return;

    // Add scroll and resize listeners
    scrollRef.addEventListener('scroll', measure, { passive: true });
    if (typeof window !== 'undefined' && scrollRef === window) {
      scrollRef.addEventListener('resize', measure);
    }
    // Initial measurement
    measure();

    return () => {
      scrollRef.removeEventListener('scroll', measure);
      if (typeof window !== 'undefined' && scrollRef === window) {
        scrollRef.removeEventListener('resize', measure);
      }
      if ('cancel' in measure) {
        measure.cancel();
      }
    };
  }, [ref, scrollRef, measure, enabled]);

  const result = [setRef, box, setScrollRef] as ClientBoxReturn;

  // Support object destructuring
  result.ref = result[0];
  result.box = result[1];
  result.scrollRef = result[2];

  return result;
}
