import {
  useCallback,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { createPortal } from 'react-dom';

import type { TransitionInOut2Api } from '~/src/components/TransitionInOut2';
import type { CSSProperties, FC, ReactNode, RefObject } from 'react';

import TransitionInOut2 from '~/src/components/TransitionInOut2';
import useKeyboard from '~/src/hooks/useKeyboard';
import useOnWindowResize from '~/src/hooks/useOnWindowResize';

const useElementRect = (elRef: RefObject<HTMLElement | null>) => {
  const [rect, setRect] = useState<DOMRect>();

  const getRect = (elRef: RefObject<HTMLElement | null>) =>
    elRef.current?.getBoundingClientRect();

  useOnWindowResize({
    onStart: useCallback(() => {
      setRect(undefined);
    }, []),

    onEnd: useCallback(() => {
      setRect(getRect(elRef));
    }, []),
  });

  useEffect(() => {
    setRect(getRect(elRef));
  }, [elRef]);

  return rect;
};

const useDialogPosition = (
  dialogRef: RefObject<HTMLElement | null>,
  targetRect: DOMRect | undefined,
  xPosition: AnchoredDialogProps['xPosition'],
  yPosition: AnchoredDialogProps['yPosition']
) => {
  const [position, setPosition] = useState<
    | {
        x: NonNullable<AnchoredDialogProps['xPosition']>;
        y: NonNullable<AnchoredDialogProps['yPosition']>;

        top: number;
        left: number;

        triangle: number;
      }
    | undefined
  >(undefined);

  // synchronously calculate the position to show dialog with proper sizing/positioning at once
  useLayoutEffect(() => {
    const dialogRect = dialogRef.current?.getBoundingClientRect();

    // unable to calculate dialog position without rects
    if (!dialogRect || !targetRect) {
      setPosition(undefined);
      return;
    }

    // the auto positioning checks if dialog does not fit inside the screen left/bottom
    // in edge case situation when dialog does not fit both sides we show it default left/bottom
    const autoXPosition =
      targetRect.left + dialogRect.width > innerWidth &&
      targetRect.right > dialogRect.width
        ? 'right'
        : 'left';

    const autoYPosition =
      targetRect.bottom + dialogRect.height > innerHeight &&
      targetRect.top > dialogRect.height
        ? 'top'
        : 'bottom';

    // we use provided position or auto calculated
    const x = xPosition || autoXPosition;
    const y = yPosition || autoYPosition;

    setPosition({
      x,
      y,

      top:
        y === 'top'
          ? targetRect.y - dialogRect.height
          : targetRect.y + targetRect.height,

      left:
        x === 'right'
          ? targetRect.x + targetRect.width - dialogRect.width
          : targetRect.x,

      triangle:
        x === 'right'
          ? dialogRect.width - targetRect.width / 2
          : targetRect.width / 2,
    });
  }, [xPosition, yPosition, targetRect]);

  return position;
};

const Triangle = ({
  size,
  style,
  nodeRef,
}: {
  size: string;
  style: CSSProperties;
  nodeRef?: RefObject<HTMLDivElement | null>;
}) => (
  <div
    ref={nodeRef as any}
    style={{
      ...style,
      width: 0,
      height: 0,
      borderLeft: `${size} solid transparent`,
      borderRight: `${size} solid transparent`,
      borderBottom: `${size} solid currentColor`,
    }}
  />
);

export interface AnchoredDialogProps {
  xPosition?: 'left' | 'right';
  yPosition?: 'top' | 'bottom';
  matchTargetElWidth?: boolean;
  zIndex?: number;
  targetElRef: RefObject<HTMLElement | null>;
  withTriangle?: boolean;
  color?: string;
  renderContent: (params: { close: () => Promise<void> }) => ReactNode;
  style?: CSSProperties;
  overlayStyle?: CSSProperties;
  onClose(): void;
  testId?: string;
}

const TRIANGLE_SIZE = '0.6rem';
const TRANSITION_DURATION = 150;

const AnchoredDialog: FC<AnchoredDialogProps> = ({
  targetElRef,
  renderContent,
  onClose,
  xPosition,
  yPosition,
  matchTargetElWidth = false,
  withTriangle = true,
  color = '#000',
  style,
  overlayStyle,
  zIndex = 1,
  testId,
}) => {
  const transitionApiRef = useRef<TransitionInOut2Api>(null);
  const triangleElRef = useRef<HTMLDivElement>(null);
  const rootElRef = useRef<HTMLDivElement>(null);
  const targetRect = useElementRect(targetElRef);

  const dialogPosition = useDialogPosition(
    rootElRef,
    targetRect,
    xPosition,
    yPosition
  );

  const close = useCallback(async () => {
    await transitionApiRef.current?.setVisible(false, {
      duration: TRANSITION_DURATION,
    });

    onClose();
  }, []);

  useKeyboard({
    rootElRef,
    onEscape: close,
  });

  // synchronously set the position to show at the final place at once
  useLayoutEffect(() => {
    const transitionApi = transitionApiRef.current;

    if (!dialogPosition) {
      // we instantly hide the dialog when position does not exist
      // to avoid broken UI when target element changed position
      // but user sees animated hiding stuck on prev position
      transitionApi?.setVisible(false, { duration: 0 });
      return;
    }

    const rootEl = rootElRef.current;
    const triangleEl = triangleElRef.current;

    if (!rootEl || !triangleEl) {
      transitionApi?.setVisible(false, { duration: 0 });
      return;
    }

    rootEl.style.top = `${dialogPosition.top}px`;
    rootEl.style.left = `${dialogPosition.left}px`;

    triangleEl.style.marginLeft = `calc(${dialogPosition.triangle}px - ${TRIANGLE_SIZE})`;

    if (dialogPosition.y === 'bottom') {
      triangleEl.style.order = '-1';
      triangleEl.style.transform = 'rotate(0)';
    } else {
      triangleEl.style.order = '1';
      triangleEl.style.transform = 'rotate(180deg)';
    }

    transitionApi?.setVisible(true, { duration: TRANSITION_DURATION });
  }, [dialogPosition]);

  return createPortal(
    <div
      style={{
        position: 'fixed',
        left: 0,
        top: 0,
        right: 0,
        bottom: 0,
        zIndex,
        ...overlayStyle,
      }}
      data-testid={testId}
      onClick={useCallback(async () => {
        await close();
        onClose();
      }, [])}
    >
      <TransitionInOut2
        isVisibleInitial={false}
        apiRef={transitionApiRef}
        nodeRef={rootElRef}
        padding="0.5rem 0"
        style={{
          position: 'absolute',

          width:
            matchTargetElWidth && targetRect?.width
              ? targetRect?.width
              : 'fit-content',

          height: 'fit-content',
          filter:
            'drop-shadow(0px 0px 1px rgba(255,255,255,0.5)) drop-shadow(0px 0px 20px rgba(0,0,0,0.8))',
        }}
        flexColumn
        styleFrom={useMemo(
          () => ({
            // yPosition used here instead of dialogPosition.y because
            // dialog gets visible before styleFrom prop updates
            // that is cause animation issue
            transform: `translateY(${yPosition === 'top' ? '8px' : '-8px'})`,
          }),
          []
        )}
      >
        <Triangle
          nodeRef={triangleElRef}
          size={TRIANGLE_SIZE}
          style={{
            color,
            margin: '0 0 -1px auto',
            opacity: withTriangle ? 1 : 0,
          }}
        />
        <div
          style={{
            backgroundColor: color,
            borderRadius: 6,
            ...style,
          }}
          onClick={useCallback((event) => {
            event.stopPropagation();
          }, [])}
        >
          {renderContent({ close })}
        </div>
      </TransitionInOut2>
    </div>,
    document.body
  );
};

export default AnchoredDialog;
