import { useState, useEffect, forwardRef } from 'react';

type Position = 0 | 1 | 2 | 3;

function isOverlappingScreen(el: Element, margin = 0) {
  const { clientWidth, clientHeight } = document.documentElement;
  const rect = el.getBoundingClientRect();

  const bottom = Math.round(rect.bottom);
  const right = Math.round(rect.right);
  const left = Math.round(rect.left);
  const top = Math.round(rect.top);

  const over_bottom = bottom > clientHeight - margin;
  const over_right = right > clientWidth - margin;
  const over_left = left < 0 + margin;
  const over_top = top < 0 + margin;

  const res = {
    isOverlapping: over_top || over_right || over_bottom || over_left,
    top: over_top ? 0 + margin - top : 0,
    right: over_right ? right - (clientWidth - margin) : 0,
    bottom: over_bottom ? bottom - (clientHeight - margin) : 0,
    left: over_left ? 0 + margin - left : 0,
  };

  return res;
}

function getPosition(placement: Position) {
  let position;
  switch (+placement) {
    case 0:
      position = 'bottom';
      break;
    case 1:
      position = 'top';
      break;
    case 2:
      position = 'right';
      break;
    case 3:
      position = 'left';
      break;
    default:
      position = 'top';
  }
  return { [position]: '100%' };
}

function alignItems(placement: Position, alignment: Position) {
  if (+placement === 2) {
    return 'flex-end';
  } else if (+placement === 3) {
    return 'flex-start';
  } else {
    switch (+alignment) {
      case 0:
        return 'flex-start';
      case 1:
        return 'center';
      case 2:
        return 'flex-end';
      default:
        return 'center';
    }
  }
}

function justifyContent(placement: Position, alignment: Position) {
  if (+placement > 1) {
    switch (+alignment) {
      case 0:
        return 'flex-start';
      case 1:
        return 'center';
      case 2:
        return 'flex-end';
      default:
        return 'center';
    }
  } else {
    switch (+placement) {
      case 0:
        return 'flex-end';
      case 1:
        return 'flex-start';
      case 2:
        return 'center';
      case 3:
        return 'center';
      default:
        return 'center';
    }
  }
}

function getAnchorStyles(anchor: Element): React.CSSProperties {
  const rect = anchor.getBoundingClientRect();

  return {
    position: 'fixed',
    zIndex: 999999999,

    width: rect.width,
    height: rect.height,
    top: rect.top,
    left: rect.left,

    pointerEvents: 'none',
  };
}

function getAlignment(
  placement: Position,
  alignment: Position
): React.CSSProperties {
  return {
    position: 'absolute',
    top: 0,
    left: 0,
    zIndex: 99999,
    width: '100%',
    height: '100%',
    display: 'flex',
    flexDirection: 'column',

    justifyContent: justifyContent(placement, alignment),
    alignItems: alignItems(placement, alignment),
  };
}

function getPlacement(
  placement: Position,
  alignment: Position
): React.CSSProperties {
  return {
    ...(alignment === 3 && { width: '100%' }),
    position: 'relative',
    ...getPosition(placement),

    pointerEvents: 'auto',
  };
}

function useOverlappingObserver(
  ref: any,
  placement: Position,
  alignment: Position
) {
  const [place, setPlacement] = useState(placement);
  const [align, setAlignment] = useState(alignment);

  useEffect(() => {
    const bounds = isOverlappingScreen(ref.current, 0);

    const { isOverlapping, ...pos } = bounds;

    if (isOverlapping) {
      const { top, bottom, left, right } = pos;

      if (top && placement === 0) {
        setPlacement(1);
      } else if (bottom && placement === 1) {
        setPlacement(0);
      } else if (left && placement === 2) {
        setPlacement(3);
      } else if (right && placement === 3) {
        setPlacement(2);
      }

      if (placement > 1) {
        if (top) {
          setAlignment(0);
        }
        if (bottom) {
          setAlignment(2);
        }
      } else {
        if (right) {
          setAlignment(2);
        }
        if (left) {
          setAlignment(0);
        }
      }
    }
  }, [ref, placement]);

  return [place, align];
}

function useMountedState() {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  return mounted;
}

type PopupProps = {
  anchor: Element;
  children: React.ReactNode;
  placement?: Position;
  alignment?: Position;
  events?: {
    onMouseEnter: React.MouseEventHandler;
    onMouseLeave: React.MouseEventHandler;
  };
};

export const Popup = forwardRef<HTMLDivElement, PopupProps>(
  ({ anchor, placement = 1, alignment = 1, children, events }, ref) => {
    const [place, align] = useOverlappingObserver(ref, placement, alignment);
    const mounted = useMountedState();
    return (
      <div
        style={{
          ...getAnchorStyles(anchor),
          visibility: mounted ? 'visible' : 'hidden',
        }}
        {...events}
      >
        <div style={getAlignment(place, align)}>
          <div style={getPlacement(place, align)} ref={ref}>
            {children}
          </div>
        </div>
      </div>
    );
  }
);
