import { useState, useCallback, RefObject, useLayoutEffect, useRef } from 'react';

type Size = {
  width: number;
  height: number;
};

const getInitialSize = (ref?: RefObject<HTMLElement | null>): Size => {
  if (ref?.current) {
    return {
      width: ref.current.offsetWidth,
      height: ref.current.offsetHeight,
    };
  }

  if (ref?.current === null) {
    return {
      width: 0,
      height: 0,
    };
  }

  return {
    width: typeof window !== 'undefined' ? window.innerWidth : 0,
    height: typeof window !== 'undefined' ? window.innerHeight : 0,
  };
};

export function useElementSize<T extends HTMLElement = HTMLElement>({
  delay = 50,
  detectSizes = 'both',
}: { delay?: number; detectSizes?: 'width' | 'height' | 'both' } = {}): [
  Size,
  RefObject<T | null>,
] {
  const ref = useRef<T | null>(null);
  const previousSize = useRef<Size>(getInitialSize(ref));
  const [size, setSize] = useState<Size>(getInitialSize(ref));
  const isFirstUpdate = useRef(true);

  const updateSize = useCallback(
    (newSize: Size) => {
      const shouldUpdateWidth = detectSizes === 'both' || detectSizes === 'width';
      const shouldUpdateHeight = detectSizes === 'both' || detectSizes === 'height';

      const widthChanged = shouldUpdateWidth && newSize.width !== previousSize.current.width;
      const heightChanged = shouldUpdateHeight && newSize.height !== previousSize.current.height;

      if (widthChanged || heightChanged) {
        const updatedSize = {
          width: shouldUpdateWidth ? newSize.width : previousSize.current.width,
          height: shouldUpdateHeight ? newSize.height : previousSize.current.height,
        };

        setSize(updatedSize);
        previousSize.current = updatedSize;
      }
    },
    [detectSizes],
  );

  useLayoutEffect(() => {
    let timeoutId: number | null = null;

    const triggerUpdateWithThrottling = (newSize: Size) => {
      if (isFirstUpdate.current) {
        updateSize(newSize);
        isFirstUpdate.current = false;
        return;
      }

      if (timeoutId !== null) return;
      timeoutId = window.setTimeout(() => {
        updateSize(newSize);
        timeoutId = null;
      }, delay);
    };

    if (ref?.current && typeof ResizeObserver !== 'undefined') {
      const observer = new ResizeObserver((entries) => {
        for (const entry of entries) {
          const { width, height } = entry.contentRect;
          triggerUpdateWithThrottling({ width, height });
        }
      });
      observer.observe(ref.current);

      return () => {
        observer.disconnect();
        if (timeoutId !== null) {
          clearTimeout(timeoutId);
        }
      };
    }
  }, [ref, updateSize, delay]);

  return [size, ref];
}
