// Gravity word-physics. Bouncy draggable pills.
// Inspired by 21st.dev/community/components/danielpetho/gravity
//
// Words drop in once, settle, and freeze (no perpetual micro-bounce).
// On drag the loop wakes up; when the dragged word is released and
// everything comes to rest, the loop sleeps again.

const { useEffect, useRef, useState } = React;

function Gravity({ words }) {
  const containerRef = useRef(null);
  const bodiesRef = useRef([]);
  const rafRef = useRef(null);
  const dragRef = useRef(null);
  const sleepingRef = useRef(false);
  const mouseRef = useRef({ x: 0, y: 0, prevX: 0, prevY: 0 });

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    // Create bodies arranged across the bottom, at rest from the start.
    // The user drags to play; physics only runs while something's in motion.
    const rect = container.getBoundingClientRect();
    const els = container.querySelectorAll(".gword");
    const bodies = [];
    const FLOOR_PAD_INIT = 14;
    const floor = rect.height - FLOOR_PAD_INIT;
    // Build bottom-aligned rows; pack widest items first so layout is dense.
    const items = Array.from(els).map((el, i) => ({ el, i, w: el.offsetWidth, h: el.offsetHeight }));
    // Greedy bin-pack: put each item on the lowest row that has space.
    const rows = [];
    items.forEach((it) => {
      let placedRow = -1;
      for (let r = 0; r < rows.length; r++) {
        const used = rows[r].reduce((s, x) => s + x.w + 14, 0);
        if (used + it.w + 14 <= rect.width) { placedRow = r; break; }
      }
      if (placedRow === -1) { rows.push([it]); }
      else { rows[placedRow].push(it); }
    });
    rows.forEach((row, r) => {
      const totalW = row.reduce((s, x) => s + x.w, 0) + (row.length - 1) * 14;
      let cursor = (rect.width - totalW) / 2;
      const rowH = Math.max(...row.map((x) => x.h));
      const y = floor - (rows.length - r) * (rowH + 6) + rowH;
      row.forEach((it) => {
        const x = cursor + (Math.random() - 0.5) * 8;
        const yJitter = y - it.h + (Math.random() - 0.5) * 6;
        bodies[it.i] = {
          el: it.el,
          x, y: yJitter,
          vx: 0, vy: 0,
          w: it.w, h: it.h,
          rot: (Math.random() - 0.5) * 24,
          vr: 0,
          radius: Math.max(it.w, it.h) / 2,
        };
        cursor += it.w + 14;
      });
    });
    bodiesRef.current = bodies;

    const gravity = (window.__GRAVITY_G ?? 0.5);
    const friction = 0.985;
    const restitution = (window.__GRAVITY_R ?? 0.6);
    // Floor margin keeps the rotated bounding-box of each pill from clipping
    // below the visible canvas bottom.
    const FLOOR_PAD = 14;
    // Sleep thresholds: when every body is below these, the loop stops.
    const REST_VX = 0.1, REST_VY = 0.5, REST_VR = 0.25;
    let restFrames = 0;

    const applyTransforms = () => {
      for (const b of bodies) {
        b.el.style.transform = `translate(${b.x}px, ${b.y}px) rotate(${b.rot}deg)`;
      }
    };

    const step = () => {
      const r = container.getBoundingClientRect();
      const W = r.width;
      const H = r.height;

      for (let i = 0; i < bodies.length; i++) {
        const b = bodies[i];
        if (dragRef.current && dragRef.current.body === b) continue;

        b.vy += gravity;
        b.vx *= friction;
        b.vy *= 0.997;
        b.x += b.vx;
        b.y += b.vy;
        b.rot += b.vr;
        b.vr *= 0.98;

        // Walls
        if (b.x < 0) { b.x = 0; b.vx = -b.vx * restitution; b.vr += b.vy * 0.05; }
        if (b.x + b.w > W) { b.x = W - b.w; b.vx = -b.vx * restitution; b.vr -= b.vy * 0.05; }
        // Floor: keep the rotated bbox above the visible bottom.
        const FLOOR = H - FLOOR_PAD;
        if (b.y + b.h > FLOOR) {
          b.y = FLOOR - b.h;
          b.vy = -b.vy * restitution;
          b.vx *= 0.92;
          b.vr *= 0.85;
          // Snap tiny floor-bounces to dead still so the word doesn't jitter forever.
          if (Math.abs(b.vy) < gravity * 1.2) {
            b.vy = 0;
            b.vx *= 0.5;
            b.vr *= 0.4;
            if (Math.abs(b.vx) < REST_VX) b.vx = 0;
            if (Math.abs(b.vr) < REST_VR) b.vr = 0;
          }
        }
        if (b.y < -400) b.y = -400;
      }

      // Collisions (axis-aligned circle approximation)
      for (let i = 0; i < bodies.length; i++) {
        for (let j = i + 1; j < bodies.length; j++) {
          const a = bodies[i], c = bodies[j];
          const ax = a.x + a.w / 2, ay = a.y + a.h / 2;
          const bx = c.x + c.w / 2, by = c.y + c.h / 2;
          const dx = bx - ax, dy = by - ay;
          const dist = Math.hypot(dx, dy);
          const minDist = (a.radius + c.radius) * 0.85;
          if (dist > 0 && dist < minDist) {
            const nx = dx / dist, ny = dy / dist;
            const overlap = (minDist - dist) / 2;
            a.x -= nx * overlap; a.y -= ny * overlap;
            c.x += nx * overlap; c.y += ny * overlap;
            const dvx = c.vx - a.vx;
            const dvy = c.vy - a.vy;
            const vn = dvx * nx + dvy * ny;
            if (vn < 0) {
              const imp = -(1 + 0.5) * vn / 2;
              a.vx -= imp * nx; a.vy -= imp * ny;
              c.vx += imp * nx; c.vy += imp * ny;
              a.vr += (Math.random() - 0.5) * 1.2;
              c.vr += (Math.random() - 0.5) * 1.2;
            }
          }
        }
      }

      applyTransforms();

      // Sleep check: every body must be slow AND in the lower half of the
      // canvas (i.e. it has actually fallen and come to rest, not just
      // momentarily slow at the apex of a bounce).
      const FLOOR = H - FLOOR_PAD;
      if (!dragRef.current) {
        const allResting = bodies.every((b) =>
          Math.abs(b.vx) < REST_VX &&
          Math.abs(b.vy) < REST_VY &&
          Math.abs(b.vr) < REST_VR &&
          (b.y + b.h >= FLOOR - 4)
        );
        if (allResting) {
          restFrames++;
          if (restFrames > 20) {
            // Hard-zero velocities so resume from drag is clean
            bodies.forEach((b) => { b.vx = 0; b.vy = 0; b.vr = 0; });
            sleepingRef.current = true;
            rafRef.current = null;
            return;
          }
        } else {
          restFrames = 0;
        }
      }

      rafRef.current = requestAnimationFrame(step);
    };

    const wake = () => {
      if (rafRef.current) return;
      sleepingRef.current = false;
      restFrames = 0;
      rafRef.current = requestAnimationFrame(step);
    };

    // Words start at rest. Paint them in place once, then sleep.
    applyTransforms();
    sleepingRef.current = true;

    // Drag
    const onPointerDown = (e) => {
      const target = e.target.closest(".gword");
      if (!target) return;
      const body = bodies.find((b) => b.el === target);
      if (!body) return;
      const rect = container.getBoundingClientRect();
      const px = e.clientX - rect.left;
      const py = e.clientY - rect.top;
      dragRef.current = { body, offsetX: px - body.x, offsetY: py - body.y };
      mouseRef.current = { x: px, y: py, prevX: px, prevY: py };
      try { target.setPointerCapture(e.pointerId); } catch (err) { /* ignore */ }
      wake();
    };
    const onPointerMove = (e) => {
      if (!dragRef.current) return;
      const rect = container.getBoundingClientRect();
      const px = e.clientX - rect.left;
      const py = e.clientY - rect.top;
      mouseRef.current.prevX = mouseRef.current.x;
      mouseRef.current.prevY = mouseRef.current.y;
      mouseRef.current.x = px;
      mouseRef.current.y = py;
      const b = dragRef.current.body;
      b.x = px - dragRef.current.offsetX;
      b.y = py - dragRef.current.offsetY;
      b.vx = mouseRef.current.x - mouseRef.current.prevX;
      b.vy = mouseRef.current.y - mouseRef.current.prevY;
      // Apply directly while dragging (loop may be sleeping)
      if (sleepingRef.current) {
        b.el.style.transform = `translate(${b.x}px, ${b.y}px) rotate(${b.rot}deg)`;
      }
    };
    const onPointerUp = () => {
      if (dragRef.current) {
        dragRef.current = null;
        wake(); // let physics carry the throw and re-settle
      }
    };

    // On mobile / touch devices we keep the words decorative only.
    // Letting users drag them on a phone hijacks vertical scroll and just
    // gets in the way. Words still render at rest from the bin-pack init.
    const isMobile =
      window.matchMedia("(max-width: 900px)").matches ||
      window.matchMedia("(hover: none) and (pointer: coarse)").matches;
    if (!isMobile) {
      container.addEventListener("pointerdown", onPointerDown);
      window.addEventListener("pointermove", onPointerMove);
      window.addEventListener("pointerup", onPointerUp);
    } else {
      container.style.cursor = "default";
    }

    // Re-throw on resize so words don't end up off-screen
    const onResize = () => {
      const r = container.getBoundingClientRect();
      bodies.forEach((b) => {
        if (b.x + b.w > r.width) b.x = r.width - b.w - 10;
        if (b.y + b.h > r.height) b.y = r.height - b.h;
      });
      applyTransforms();
      wake();
    };
    window.addEventListener("resize", onResize);

    return () => {
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
      if (!isMobile) {
        container.removeEventListener("pointerdown", onPointerDown);
        window.removeEventListener("pointermove", onPointerMove);
        window.removeEventListener("pointerup", onPointerUp);
      }
      window.removeEventListener("resize", onResize);
    };
  }, [words]);

  return (
    <div ref={containerRef} className="gravity-canvas">
      {words.map((w, i) => (
        <div
          key={i}
          className={`gword ${w.style || ""}`}
          style={{
            fontSize: w.size || 24,
            fontStyle: w.italic === false ? "normal" : "italic",
            fontFamily: w.mono ? "var(--mono)" : "var(--serif)",
            letterSpacing: w.mono ? ".15em" : "normal",
            textTransform: w.mono ? "uppercase" : "none",
            transform: "translate(-9999px, -9999px)",
          }}
        >
          {w.text}
        </div>
      ))}
    </div>
  );
}

window.Gravity = Gravity;
