import type { CSSProperties, RefObject } from "react";
import { useCallback, useLayoutEffect } from "react";

type Placement = "bottom" | "top";

interface UseFloatingPositionArgs {
  referenceRef: RefObject<HTMLElement | null>;
  floatingRef: RefObject<HTMLDivElement | null>;
  arrowRef: RefObject<SVGSVGElement | null>;
  isOpen: boolean;
  offsetPx: number;
  arrowWidth: number;
  useFixedStrategy: boolean;
}

interface UseFloatingPositionResult {
  initialFloatingStyles: CSSProperties;
  initialArrowStyles: CSSProperties;
  update: () => void;
}

// Two positioning strategies:
// - `absolute` + document-relative coords (viewport rect + window.scrollY/X)
//   when portalled to document.body. The initial containing block is the
//   document, so absolute coords pin to the trigger and scroll natively.
// - `fixed` + viewport coords when portalled inside a <dialog> (top-layer).
//   The dialog's own box would otherwise become the containing block for an
//   `absolute` child, throwing off coordinates and triggering `overflow: hidden`
//   clipping. `fixed` keeps the containing block as the viewport.
const ABSOLUTE_FLOATING_STYLES: CSSProperties = {
  position: "absolute",
  top: 0,
  left: 0,
  width: "max-content",
};

const FIXED_FLOATING_STYLES: CSSProperties = {
  position: "fixed",
  top: 0,
  left: 0,
  width: "max-content",
};

const INITIAL_ARROW_STYLES: CSSProperties = {
  position: "absolute",
};

const VIEWPORT_PADDING = 0;

// Properties that make an ancestor the containing block for a `position: fixed`
// descendant, overriding the initial viewport containing block. When the portal
// target (dialog) has any of these, `position: fixed` coords are interpreted
// relative to the dialog's box, not the viewport — we compensate by subtracting
// the dialog's viewport offset.
const createsFixedContainingBlock = (el: HTMLElement): boolean => {
  const style = window.getComputedStyle(el);
  return (
    style.transform !== "none" ||
    style.filter !== "none" ||
    style.backdropFilter !== "none" ||
    style.perspective !== "none" ||
    /\b(transform|filter|perspective)\b/.test(style.willChange)
  );
};

export const useFloatingPosition = ({
  referenceRef,
  floatingRef,
  arrowRef,
  isOpen,
  offsetPx,
  arrowWidth,
  useFixedStrategy,
}: UseFloatingPositionArgs): UseFloatingPositionResult => {
  // Writes styles directly to the DOM (bypassing React state) so scroll/resize
  // handlers can update position in the same event-frame the browser uses to
  // paint the scroll, eliminating the re-render lag that shows up as a
  // trigger-vs-popover parallax on fast scroll.
  const update = useCallback(() => {
    const reference = referenceRef.current;
    const floating = floatingRef.current;
    if (!reference || !floating) return;

    const refRect = reference.getBoundingClientRect();
    const floatingWidth = floating.offsetWidth;
    const floatingHeight = floating.offsetHeight;
    const scrollX = useFixedStrategy ? 0 : window.scrollX;
    const scrollY = useFixedStrategy ? 0 : window.scrollY;

    // Clamp to the portal target's bounds when the dialog is a containing
    // block (its own `overflow: hidden` would clip the popover). Otherwise
    // clamp to the viewport.
    const parent = floating.parentElement;
    const parentIsCb =
      useFixedStrategy && !!parent && createsFixedContainingBlock(parent);
    const boundsRect = parentIsCb
      ? parent.getBoundingClientRect()
      : {
          left: 0,
          top: 0,
          right: window.innerWidth,
          bottom: window.innerHeight,
        };

    const bottomTopViewport = refRect.bottom + offsetPx;
    const topTopViewport = refRect.top - floatingHeight - offsetPx;
    const overflowsBottom =
      bottomTopViewport + floatingHeight > boundsRect.bottom;
    const fitsTop = topTopViewport >= boundsRect.top;
    const nextPlacement: Placement =
      overflowsBottom && fitsTop ? "top" : "bottom";

    const topViewport =
      nextPlacement === "bottom" ? bottomTopViewport : topTopViewport;

    const rawLeftViewport =
      refRect.left + refRect.width / 2 - floatingWidth / 2;
    const minLeftViewport = boundsRect.left + VIEWPORT_PADDING;
    const maxLeftViewport = Math.max(
      minLeftViewport,
      boundsRect.right - floatingWidth - VIEWPORT_PADDING,
    );
    const leftViewport = Math.min(
      Math.max(minLeftViewport, rawLeftViewport),
      maxLeftViewport,
    );

    // Subtract containing-block offset when the dialog is the CB, so
    // `position: fixed` coords land at the intended viewport position.
    const cbOffsetLeft = parentIsCb ? boundsRect.left : 0;
    const cbOffsetTop = parentIsCb ? boundsRect.top : 0;

    floating.style.top = `${topViewport + scrollY - cbOffsetTop}px`;
    floating.style.left = `${leftViewport + scrollX - cbOffsetLeft}px`;

    const arrow = arrowRef.current;
    if (!arrow) return;

    // Arrow coords are relative to the floating element, so scroll cancels out.
    const arrowHalf = arrowWidth / 2;
    const refCenterX = refRect.left + refRect.width / 2;
    const rawArrowLeft = refCenterX - leftViewport - arrowHalf;
    const maxArrowLeft = Math.max(0, floatingWidth - arrowWidth);
    const arrowLeft = Math.min(Math.max(0, rawArrowLeft), maxArrowLeft);

    arrow.style.left = `${arrowLeft}px`;
    if (nextPlacement === "bottom") {
      arrow.style.bottom = "100%";
      arrow.style.top = "";
      arrow.style.transform = "rotate(180deg)";
    } else {
      arrow.style.top = "100%";
      arrow.style.bottom = "";
      arrow.style.transform = "";
    }
  }, [
    referenceRef,
    floatingRef,
    arrowRef,
    offsetPx,
    arrowWidth,
    useFixedStrategy,
  ]);

  useLayoutEffect(() => {
    if (!isOpen) return;
    update();
  }, [isOpen, update]);

  return {
    initialFloatingStyles: useFixedStrategy
      ? FIXED_FLOATING_STYLES
      : ABSOLUTE_FLOATING_STYLES,
    initialArrowStyles: INITIAL_ARROW_STYLES,
    update,
  };
};
