import { useEffect, useId, useLayoutEffect, useRef } from "react";
import type { ReactNode } from "react";
import { createPortal } from "react-dom";
import { useLift } from "./LiftContext";

/** Recursively strip CSS animations and transitions from a cloned DOM tree */
function stripAnimations(node: Node) {
  if (node instanceof HTMLElement) {
    node.style.animation = "none";
    node.style.transition = "none";
    node.getAnimations().forEach((a) => a.cancel());
  }
  for (const child of Array.from(node.childNodes)) {
    stripAnimations(child);
  }
}

function cloneWithoutAnimations(source: HTMLElement, target: HTMLElement) {
  target.innerHTML = "";
  for (const child of Array.from(source.children)) {
    const clone = child.cloneNode(true) as HTMLElement;
    stripAnimations(clone);
    target.appendChild(clone);
  }
}

type LiftableProps = {
  id?: string;
  always?: boolean;
};

type Props = LiftableProps & {
  children: ReactNode;
};

export function useLiftable<T extends Record<string, unknown> = Record<string, never>>(
  render: (props: { isLifted: boolean } & T) => ReactNode,
  options?: LiftableProps & T,
): { id: string; element: ReactNode } {
  const autoId = useId();
  const {
    id: idProp,
    always,
    ...extraProps
  } = (options ?? {}) as LiftableProps & Record<string, unknown>;
  const id = idProp ?? autoId;
  const { liftedIds, alwaysLiftedIds, modalOpen, registerAlways, unregisterAlways } = useLift();

  useEffect(() => {
    if (always) {
      registerAlways(id);
      return () => unregisterAlways(id);
    }
  }, [always, id, registerAlways, unregisterAlways]);

  const isLifted = liftedIds.has(id) || (alwaysLiftedIds.has(id) && modalOpen);

  const element = (
    <Liftable id={id}>{render({ isLifted, ...extraProps } as { isLifted: boolean } & T)}</Liftable>
  );

  return { id, element };
}

export { cloneWithoutAnimations };

export function Liftable({ id: idProp, always, children }: Props) {
  const autoId = useId();
  const id = idProp ?? autoId;
  const {
    liftedIds,
    alwaysLiftedIds,
    modalOpen,
    registerAlways,
    unregisterAlways,
    portalContainer,
  } = useLift();

  const childrenHostRef = useRef<HTMLDivElement>(null);
  const inlineSlotRef = useRef<HTMLDivElement>(null);
  const portalWrapperRef = useRef<HTMLDivElement>(null);
  const wasLiftedRef = useRef(false);

  useEffect(() => {
    if (always) {
      registerAlways(id);
      return () => unregisterAlways(id);
    }
  }, [always, id, registerAlways, unregisterAlways]);

  const isLifted = liftedIds.has(id) || (alwaysLiftedIds.has(id) && modalOpen);

  useLayoutEffect(() => {
    const host = childrenHostRef.current;
    const inlineSlot = inlineSlotRef.current;
    const portalWrapper = portalWrapperRef.current;
    if (!host || !inlineSlot || !portalWrapper) return;

    if (isLifted && !wasLiftedRef.current) {
      const rect = inlineSlot.getBoundingClientRect();
      cloneWithoutAnimations(host, inlineSlot);
      portalWrapper.appendChild(host);
      portalWrapper.style.cssText = `position:fixed;top:${rect.top}px;left:${rect.left}px;width:${rect.width}px;height:${rect.height}px;pointer-events:auto`;
      wasLiftedRef.current = true;
    } else if (!isLifted && wasLiftedRef.current) {
      inlineSlot.innerHTML = "";
      inlineSlot.appendChild(host);
      portalWrapper.style.cssText = "display:none";
      wasLiftedRef.current = false;
    }
  }, [isLifted]);

  useEffect(() => {
    if (!isLifted || !inlineSlotRef.current || !portalWrapperRef.current) return;
    const portalWrapper = portalWrapperRef.current;
    const inlineSlot = inlineSlotRef.current;

    const measure = () => {
      const rect = inlineSlot.getBoundingClientRect();
      portalWrapper.style.top = `${rect.top}px`;
      portalWrapper.style.left = `${rect.left}px`;
      portalWrapper.style.width = `${rect.width}px`;
      portalWrapper.style.height = `${rect.height}px`;
    };

    const observer = new ResizeObserver(measure);
    observer.observe(inlineSlot);
    window.addEventListener("resize", measure);

    return () => {
      observer.disconnect();
      window.removeEventListener("resize", measure);
    };
  }, [isLifted]);

  return (
    <>
      <div ref={inlineSlotRef}>
        <div ref={childrenHostRef}>{children}</div>
      </div>
      {portalContainer &&
        createPortal(<div ref={portalWrapperRef} style={{ display: "none" }} />, portalContainer)}
    </>
  );
}
