import {
  cloneElement,
  isValidElement,
  type JSXElementConstructor,
  type ReactElement,
  useEffect,
  useLayoutEffect,
  useRef,
  useState,
} from 'react';

export const Positions = {
  top: 'top',
  topLeft: 'topLeft',
  topRight: 'topRight',
  bottom: 'bottom',
  bottomLeft: 'bottomLeft',
  bottomRight: 'bottomRight',
  left: 'left',
  leftTop: 'leftTop',
  leftBottom: 'leftBottom',
  right: 'right',
  rightTop: 'rightTop',
  rightBottom: 'rightBottom',
} as const;

interface useOverlayProps {
  referenceElement: React.RefObject<HTMLElement | null>;
  children: ReactElement<unknown, string | JSXElementConstructor<any>>;
  position: (typeof Positions)[keyof typeof Positions];
  onOverlayClick?: () => void;
  offset?: {
    x?: number;
    y?: number;
  };
  style?: {
    maxHeight?: number;
    maxWidth?: number;
  };
}

export default function Overlay(props: useOverlayProps) {
  const [style, setStyle] = useState<React.CSSProperties>({
    top: 0,
    left: 0,
    transform: '',
    position: 'fixed',
  });

  const overlayRef = useRef<HTMLDivElement>(null);

  const getMaxHeight = () => {
    const refEl = props.referenceElement.current;

    if (!refEl) {
      return;
    }

    const refRect = refEl.getBoundingClientRect();

    let maxAvailableHeight = 0;
    const windowHeight = window.innerHeight;

    switch (props.position) {
      case Positions.bottom:
      case Positions.bottomLeft:
      case Positions.bottomRight:
        maxAvailableHeight = windowHeight - refRect.bottom - (props.offset?.y || 0) * 2;
        break;

      case Positions.top:
      case Positions.topLeft:
      case Positions.topRight:
        maxAvailableHeight = refRect.top - (props.offset?.y || 0) * 2;
        break;

      default:
        return '100%';
    }

    if (props.style?.maxHeight) {
      maxAvailableHeight =
        maxAvailableHeight < props.style?.maxHeight ? maxAvailableHeight : props.style?.maxHeight;
    }

    return maxAvailableHeight;
  };

  const getMaxWidth = () => {
    const refEl = props.referenceElement.current;

    if (!refEl) {
      return;
    }

    const refRect = refEl.getBoundingClientRect();

    let maxAvailableWidth = 0;
    const windowWidth = window.innerWidth;

    switch (props.position) {
      case Positions.right:
      case Positions.rightBottom:
      case Positions.rightTop:
        maxAvailableWidth = windowWidth - refRect.right - (props.offset?.x || 0) * 2;
        break;

      case Positions.left:
      case Positions.leftBottom:
      case Positions.leftTop:
        maxAvailableWidth = refRect.left - (props.offset?.x || 0) * 2;
        break;

      default:
        return null;
    }

    if (props.style?.maxWidth) {
      maxAvailableWidth =
        maxAvailableWidth < props.style?.maxWidth ? maxAvailableWidth : props.style?.maxWidth;
    }

    return maxAvailableWidth;
  };

  const getUpdatedPositions = () => {
    const refEl = props.referenceElement.current;

    if (!refEl) {
      return;
    }

    const refRect = refEl.getBoundingClientRect();

    let top = 0;
    let left = 0;
    let transform = '';

    switch (props.position) {
      case Positions.top:
        top = refRect.top - (props.offset?.y || 0);
        left = refRect.left + refRect.width / 2;
        transform = 'translate(-50%, -100%)';
        break;
      case Positions.topLeft:
        top = refRect.top - (props.offset?.y || 0);
        left = refRect.left;
        transform = 'translateY(-100%)';
        break;
      case Positions.topRight:
        top = refRect.top - (props.offset?.y || 0);
        left = refRect.right;
        transform = 'translate(-100%, -100%)';
        break;
      case Positions.bottom:
        top = refRect.bottom + (props.offset?.y || 0);
        left = refRect.left + refRect.width / 2;
        transform = 'translateX(-50%)';
        break;
      case Positions.bottomLeft:
        top = refRect.bottom + (props.offset?.y || 0);
        left = refRect.left;
        break;
      case Positions.bottomRight:
        top = refRect.bottom + (props.offset?.y || 0);
        left = refRect.right;
        transform = 'translateX(-100%)';
        break;
      case Positions.left:
        top = refRect.top + refRect.height / 2;
        left = refRect.left - (props.offset?.x || 0);
        transform = 'translate(-100%,-50%)';
        break;
      case Positions.leftTop:
        top = refRect.top;
        left = refRect.left - (props.offset?.x || 0);
        transform = 'translateX(-100%)';
        break;
      case Positions.leftBottom:
        top = refRect.bottom;
        left = refRect.left - (props.offset?.x || 0);
        transform = 'translate(-100%, -100%)';
        break;
      case Positions.right:
        top = refRect.top + refRect.height / 2;
        left = refRect.right + (props.offset?.x || 0);
        transform = 'translateY(-50%)';
        break;
      case Positions.rightTop:
        top = refRect.top;
        left = refRect.right + (props.offset?.x || 0);
        break;
      case Positions.rightBottom:
        top = refRect.bottom;
        left = refRect.right + (props.offset?.x || 0);
        transform = 'translateY(-100%)';
        break;
    }

    const maxHeight = getMaxHeight();
    const maxWidth = getMaxWidth();

    setStyle((prevState) => ({
      ...prevState,
      top: top,
      left: left,
      transform,
      width: refRect.width,
      ...(maxHeight && { maxHeight: maxHeight as number }),
      ...(maxWidth && { maxWidth: maxWidth as number }),
    }));
  };

  useLayoutEffect(() => {
    if (!props.referenceElement.current) {
      return;
    }

    getUpdatedPositions();

    const updatePositions = () => getUpdatedPositions();

    const observer = new ResizeObserver(updatePositions);
    if (props.referenceElement.current) {
      observer.observe(props.referenceElement.current);
    }
    if (overlayRef.current) {
      observer.observe(overlayRef.current);
    }

    window.addEventListener('resize', updatePositions);
    window.addEventListener('scroll', updatePositions, true);

    return () => {
      observer.disconnect();
      window.removeEventListener('resize', updatePositions);
      window.removeEventListener('scroll', updatePositions, true);
    };
  }, [props.referenceElement.current]);

  if (isValidElement(props.children)) {
    return (
      <>
        {props.onOverlayClick && (
          <div
            role="button"
            className="z-2 fixed left-[0] top-[0] h-[100vh] w-[100vw]"
            onClick={props.onOverlayClick}
            onKeyDown={props.onOverlayClick}
            aria-label="Close dropdown"
            tabIndex={-1}
          />
        )}
        {cloneElement(props.children as ReactElement, { style })}
      </>
    );
  } else {
    return props.children;
  }
}
