import { useLayoutEffect, useRef } from "react";

/**
 * Cards are keyed by card id, so the same card that moves between zones (hand →
 * expedition, discard pile → hand, board → hand on undo) is the same logical
 * element even though React mounts it in a different subtree. This hook records
 * where every card was painted last render and, after the DOM updates, plays the
 * difference as real motion (FLIP): the card is placed back at its old position
 * and animated to its new one, so it travels instead of teleporting.
 *
 * A card arriving from the deck was not on screen before, so it flies out of the
 * deck stack face-down and flips over on the way in. Undoing a deck draw sends it
 * back the same way: the detached element is re-parented into an overlay and
 * animated home.
 */

const MOVE_MS = 400;
const FLIP_MS = 520;
const EASE_OUT = "cubic-bezier(0.22, 0.61, 0.36, 1)";
const EASE_IN_OUT = "cubic-bezier(0.45, 0.05, 0.55, 0.95)";
const DEAL_STAGGER_MS = 45;

export interface CardMotion {
  cardRef: (card: number) => (element: HTMLElement | null) => void;
  deckRef: (element: HTMLElement | null) => void;
  overlayRef: (element: HTMLDivElement | null) => void;
  /** Drop the recorded layout so a fresh deal animates in rather than flying
   *  every card of the previous game back to the deck. */
  resetMotion: () => void;
}

function center(rect: DOMRect): { x: number; y: number } {
  return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
}

function prefersReducedMotion(): boolean {
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

/** Slide a card from where it used to be to where it now is. */
function animateMove(element: HTMLElement, from: DOMRect, to: DOMRect): void {
  const dx = from.left - to.left;
  const dy = from.top - to.top;
  if (Math.abs(dx) < 1 && Math.abs(dy) < 1) return;
  element.animate(
    [
      { transform: `translate(${dx}px, ${dy}px)`, zIndex: 30 },
      { transform: `translate(${dx * 0.4}px, ${dy * 0.4}px) scale(1.06)`, zIndex: 30, offset: 0.5 },
      { transform: "none", zIndex: 30 },
    ],
    { duration: MOVE_MS, easing: EASE_OUT },
  );
}

/** Fly a card out of the deck and flip it face-up on the way. The cover hides
 *  the mirrored face while the card is turned past edge-on. */
function animateFromDeck(element: HTMLElement, deck: DOMRect, to: DOMRect, delay: number): void {
  const start = center(deck);
  const end = center(to);
  const dx = start.x - end.x;
  const dy = start.y - end.y;
  const timing = { duration: FLIP_MS, easing: EASE_OUT, delay, fill: "backwards" as const };
  element.animate(
    [
      {
        transform: `translate(${dx}px, ${dy}px) perspective(900px) rotateY(-180deg) scale(0.82)`,
        zIndex: 30,
      },
      { transform: "none", zIndex: 30 },
    ],
    timing,
  );
  element.querySelector<HTMLElement>(".card__flip-cover")?.animate(
    [
      { opacity: 1, offset: 0 },
      { opacity: 1, offset: 0.46 },
      { opacity: 0, offset: 0.54 },
      { opacity: 0, offset: 1 },
    ],
    timing,
  );
}

/** Undo of a deck draw: the card has left the tree, so re-parent the detached
 *  element into the overlay and fly it back into the deck, flipping face-down. */
function animateToDeck(
  element: HTMLElement,
  from: DOMRect,
  deck: DOMRect,
  overlay: HTMLDivElement,
): void {
  const start = center(from);
  const end = center(deck);
  const dx = end.x - start.x;
  const dy = end.y - start.y;

  Object.assign(element.style, {
    position: "fixed",
    left: `${from.left}px`,
    top: `${from.top}px`,
    width: `${from.width}px`,
    height: `${from.height}px`,
    margin: "0",
    pointerEvents: "none",
  });
  overlay.append(element);

  const timing = { duration: FLIP_MS, easing: EASE_IN_OUT };
  const animation = element.animate(
    [
      { transform: "none" },
      {
        transform: `translate(${dx}px, ${dy}px) perspective(900px) rotateY(180deg) scale(0.82)`,
      },
    ],
    timing,
  );
  element.querySelector<HTMLElement>(".card__flip-cover")?.animate(
    [
      { opacity: 0, offset: 0 },
      { opacity: 0, offset: 0.46 },
      { opacity: 1, offset: 0.54 },
      { opacity: 1, offset: 1 },
    ],
    timing,
  );
  animation.finished.then(() => element.remove(), () => element.remove());
}

export function useCardMotion(): CardMotion {
  const elements = useRef(new Map<number, HTMLElement>());
  const rects = useRef(new Map<number, DOMRect>());
  const deck = useRef<HTMLElement | null>(null);
  const overlay = useRef<HTMLDivElement | null>(null);
  const dealing = useRef(true);

  // No dependency list: every render is a chance for a card to have moved.
  useLayoutEffect(() => {
    const reduced = prefersReducedMotion();
    const deckRect = deck.current?.getBoundingClientRect() ?? null;
    const next = new Map<number, DOMRect>();
    let dealt = 0;

    for (const [card, element] of [...elements.current]) {
      // React re-attaches the ref when a card remounts in another zone, so a
      // still-detached element means the card left the table (back to the deck).
      if (!element.isConnected) {
        const from = rects.current.get(card);
        elements.current.delete(card);
        if (!reduced && from && deckRect && overlay.current) {
          animateToDeck(element, from, deckRect, overlay.current);
        }
        continue;
      }

      const to = element.getBoundingClientRect();
      next.set(card, to);
      if (reduced) continue;

      const from = rects.current.get(card);
      if (from) {
        animateMove(element, from, to);
      } else if (deckRect) {
        animateFromDeck(element, deckRect, to, dealing.current ? dealt * DEAL_STAGGER_MS : 0);
        dealt += 1;
      }
    }

    rects.current = next;
    dealing.current = false;
  });

  const cardRef = useRef((card: number) => (element: HTMLElement | null) => {
    // Ignore unmount (null): the element is kept so a card that left the tree can
    // still be flown back to the deck, and it is dropped once that is handled.
    if (element) elements.current.set(card, element);
  }).current;

  return {
    cardRef,
    deckRef: (element) => { deck.current = element; },
    overlayRef: (element) => { overlay.current = element; },
    resetMotion: () => {
      rects.current.clear();
      dealing.current = true;
    },
  };
}
