import React, { useEffect, useRef, useState, type CSSProperties } from "react";

type PopoverAnchor =
  | { kind: "point"; x: number; y: number }
  | { kind: "element"; element: HTMLElement };

type PopoverSide = "bottom" | "right";
type PopoverAlign = "start" | "end" | "center";

interface UseViewportPopoverOptions {
  anchor: PopoverAnchor;
  width: number;
  estimatedHeight: number;
  onClose: () => void;
  side?: PopoverSide;
  align?: PopoverAlign;
  offset?: number;
  viewportPadding?: number;
  ignoreElements?: Array<HTMLElement | null>;
  ignoreSelectors?: string[];
  ignoreScrollContainers?: React.RefObject<HTMLElement | null>[];
  zIndex?: number;
}

export function useViewportPopover({
  anchor,
  width,
  estimatedHeight,
  onClose,
  side = "bottom",
  align = "start",
  offset = 8,
  viewportPadding = 16,
  ignoreElements = [],
  ignoreSelectors = [],
  ignoreScrollContainers,
  zIndex = 9998,
}: UseViewportPopoverOptions) {
  const ref = useRef<HTMLDivElement>(null);
  const frameRef = useRef<number | null>(null);

  const computeStyle = (): CSSProperties => {
    if (typeof window === "undefined") {
      return {
        position: "fixed",
        left: viewportPadding,
        top: viewportPadding,
        width,
        zIndex,
      };
    }

    let left = 0;
    let top = 0;

    if (anchor.kind === "element") {
      const rect = anchor.element.getBoundingClientRect();
      if (side === "right") {
        left = rect.right + offset;
        if (align === "end") {
          top = rect.bottom - estimatedHeight;
        } else if (align === "center") {
          top = rect.top + rect.height / 2 - estimatedHeight / 2;
        } else {
          top = rect.top;
        }
      } else {
        left = rect.left;
        if (align === "end") {
          left = rect.right - width;
        } else if (align === "center") {
          left = rect.left + rect.width / 2 - width / 2;
        }
        top = rect.bottom + offset;
      }
    } else {
      left = anchor.x;
      top = anchor.y + offset;

      if (align === "end") {
        left = anchor.x - width;
      } else if (align === "center") {
        left = anchor.x - width / 2;
      }
    }

    const measuredWidth = ref.current?.offsetWidth ?? width;
    const measuredHeight = ref.current?.offsetHeight ?? estimatedHeight;
    const maxLeft = Math.max(viewportPadding, window.innerWidth - measuredWidth - viewportPadding);
    const maxTop = Math.max(viewportPadding, window.innerHeight - measuredHeight - viewportPadding);

    return {
      position: "fixed",
      left: Math.min(Math.max(left, viewportPadding), maxLeft),
      top: Math.min(Math.max(top, viewportPadding), maxTop),
      width,
      zIndex,
    };
  };

  const [style, setStyle] = useState<CSSProperties>(() => computeStyle());

  useEffect(() => {
    function handlePointerDown(event: PointerEvent) {
      const target = event.target;
      if (!(target instanceof Node)) {
        return;
      }
      if (ref.current?.contains(target)) {
        return;
      }
      if (ignoreElements.some((element) => element?.contains(target))) {
        return;
      }
      if (
        target instanceof Element
        && ignoreSelectors.some((selector) => target.closest(selector) !== null)
      ) {
        return;
      }
      onClose();
    }

    function handleEscape(event: KeyboardEvent) {
      if (event.key === "Escape") {
        onClose();
      }
    }

    document.addEventListener("pointerdown", handlePointerDown, true);
    window.addEventListener("keydown", handleEscape);

    return () => {
      document.removeEventListener("pointerdown", handlePointerDown, true);
      window.removeEventListener("keydown", handleEscape);
    };
  }, [ignoreElements, ignoreSelectors, onClose]);

  useEffect(() => {
    setStyle(computeStyle());
  }, [align, anchor, estimatedHeight, offset, side, viewportPadding, width, zIndex]);

  useEffect(() => {
    const element = ref.current;
    if (!element || typeof ResizeObserver === "undefined") {
      return;
    }

    const observer = new ResizeObserver(() => {
      setStyle(computeStyle());
    });
    observer.observe(element);

    return () => {
      observer.disconnect();
    };
  }, [align, anchor, estimatedHeight, offset, side, viewportPadding, width, zIndex]);

  useEffect(() => {
    const closeOnViewportChange = anchor.kind === "point";

    function cancelScheduledFrame() {
      if (frameRef.current === null) return;
      cancelAnimationFrame(frameRef.current);
      frameRef.current = null;
    }

    function updateOrClose() {
      if (anchor.kind === "element" && !anchor.element.isConnected) {
        onClose();
        return;
      }

      setStyle(computeStyle());
    }

    function scheduleUpdate(reason: "scroll" | "resize") {
      if (frameRef.current !== null) return;
      frameRef.current = requestAnimationFrame(() => {
        frameRef.current = null;
        updateOrClose();
      });
    }

    updateOrClose();

    const handleScroll = (event: Event) => {
      if (closeOnViewportChange) {
        const scrollTarget = (event as Event).target;
        if (
          ignoreScrollContainers?.some(
            (r) =>
              r.current != null &&
              scrollTarget instanceof Node &&
              r.current.contains(scrollTarget),
          )
        ) {
          return;
        }
        cancelScheduledFrame();
        onClose();
        return;
      }
      scheduleUpdate("scroll");
    };
    const handleResize = () => {
      if (closeOnViewportChange) {
        cancelScheduledFrame();
        onClose();
        return;
      }
      scheduleUpdate("resize");
    };

    window.addEventListener("scroll", handleScroll, true);
    window.addEventListener("resize", handleResize, { passive: true });
    window.visualViewport?.addEventListener("resize", handleResize, { passive: true });
    window.visualViewport?.addEventListener("scroll", handleScroll, { passive: true });

    return () => {
      cancelScheduledFrame();
      window.removeEventListener("scroll", handleScroll, true);
      window.removeEventListener("resize", handleResize);
      window.visualViewport?.removeEventListener("resize", handleResize);
      window.visualViewport?.removeEventListener("scroll", handleScroll);
    };
  }, [
    align,
    anchor,
    estimatedHeight,
    ignoreScrollContainers,
    offset,
    onClose,
    side,
    viewportPadding,
    width,
    zIndex,
  ]);

  return { ref, style };
}
