import React from 'react';
import { isValidElement, type ReactElement, 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;

export interface popoverDimensions {
  maxHeight?: number;
  maxWidth?: number;
  height?: number;
  width?: number;
}
interface useOverlayProps {
  referenceElement: React.RefObject<HTMLElement | null>;
  children: ReactElement;
  position: (typeof Positions)[keyof typeof Positions];
  onOverlayClick?: () => void;
  offset?: {
    x?: number;
    y?: number;
  };
  style?: popoverDimensions;
  isOpen?: boolean;
}

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

  const elementRef = useRef<HTMLDivElement>(null);

  const getContainingBlockOffset = (): { top: number; left: number } => {
    const probe = document.createElement('div');
    probe.style.cssText =
      'position:fixed;top:0;left:0;width:1px;height:1px;pointer-events:none;visibility:hidden;';
    document.body.appendChild(probe);
    const rect = probe.getBoundingClientRect();
    document.body.removeChild(probe);
    return { top: rect.top, left: rect.left };
  };

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

    if (!refEl) {
      return;
    }

    const refRect = refEl.getBoundingClientRect();

    let maxAvailableHeight = 0;
    const windowHeight = window.innerHeight;
    const offsetY = props.offset?.y || 0;

    switch (props.position) {
      case Positions.bottom:
      case Positions.bottomLeft:
      case Positions.bottomRight:
        maxAvailableHeight = windowHeight - refRect.bottom - offsetY * 2;
        break;

      case Positions.top:
      case Positions.topLeft:
      case Positions.topRight:
        maxAvailableHeight = refRect.top - offsetY * 2;
        break;

      default:
        maxAvailableHeight = Math.min(
          windowHeight - offsetY * 2,
          props.style?.maxHeight || Number.POSITIVE_INFINITY,
        );
    }

    if (props.style?.maxHeight) {
      maxAvailableHeight = Math.min(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;
    const offsetX = props.offset?.x || 0;

    switch (props.position) {
      case Positions.right:
      case Positions.rightBottom:
      case Positions.rightTop:
        maxAvailableWidth = windowWidth - refRect.right - offsetX * 2;
        break;

      case Positions.left:
      case Positions.leftBottom:
      case Positions.leftTop:
        maxAvailableWidth = refRect.left - offsetX * 2;
        break;

      default:
        maxAvailableWidth = Math.min(
          windowWidth - offsetX * 2,
          props.style?.maxWidth || Number.POSITIVE_INFINITY,
        );
    }

    if (props.style?.maxWidth) {
      maxAvailableWidth = Math.min(maxAvailableWidth, props.style.maxWidth);
    }

    return maxAvailableWidth;
  };

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

    if (!refEl) {
      return;
    }

    const refRect = refEl.getBoundingClientRect();

    const cbOffset = getContainingBlockOffset();

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

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

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

    setStyle((prevState) => ({
      ...prevState,
      top,
      left,
      transform,
      ...(maxHeight && { maxHeight }),
      ...(maxWidth && { maxWidth }),
    }));
  };

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

    getUpdatedPositions();

    const updatePositions = () => getUpdatedPositions();

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

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

  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}
          />
        )}
        <div
          style={{
            ...style,
            overflow: 'auto',
            width:
              props.style?.width || props.referenceElement.current?.getBoundingClientRect().width,
            height: props.style?.height || 'fit-content',
            zIndex: 30,
          }}
        >
          {React.cloneElement(props.children as ReactElement, { ref: elementRef })}
        </div>
      </>
    );
  }
  return props.children;
}
