// RevealDisk.jsx — circular timer with 3 reveal modes
// progress: 0 → 1 (0 = full disk covering image; 1 = image fully revealed)

const { useMemo } = React;

// Time-based exponential smoothing so the cover animates between the
// 5Hz progress ticks fed in from the app. Time-constant ~80ms feels
// continuous without lagging perceptibly behind the displayed digits.
function useSmoothProgress(target) {
  const [smoothed, setSmoothed] = React.useState(target);
  const stateRef = React.useRef({ value: target, target });
  React.useEffect(() => { stateRef.current.target = target; }, [target]);
  React.useEffect(() => {
    let raf = 0;
    let lastT = 0;
    const TAU = 0.08; // seconds
    const loop = (t) => {
      const s = stateRef.current;
      const dt = lastT ? Math.min(0.05, (t - lastT) / 1000) : 0.016;
      lastT = t;
      const diff = s.target - s.value;
      if (Math.abs(diff) < 0.00005) {
        s.value = s.target;
        setSmoothed(s.target);
        raf = 0;
        return;
      }
      const k = 1 - Math.exp(-dt / TAU);
      s.value += diff * k;
      setSmoothed(s.value);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => { if (raf) cancelAnimationFrame(raf); };
  }, [target]);
  return smoothed;
}

function RevealDisk({ progress, imageData, imageTransform, mode, color, gridSize = 10, pixelOrder, direction = "clockwise", softBoundary = true, edgeStyle = "sand" }) {
  const clamped = Math.min(1, Math.max(0, progress));
  const p = useSmoothProgress(clamped);

  // Image background style (with transform if present)
  const imgTransform = imageTransform
    ? `translate(-50%, -50%) translate(${imageTransform.x}px, ${imageTransform.y}px) scale(${imageTransform.scale}) rotate(${imageTransform.rotate || 0}deg)`
    : "translate(-50%, -50%)";

  return (
    <>
      {imageData ? (
        <div
          className="disk-image"
          style={{
            backgroundImage: `url(${imageData})`,
            // Apply transform via positioning + scaling
            ...(imageTransform ? {
              backgroundSize: `${100 * imageTransform.scale}% ${100 * imageTransform.scale}%`,
              backgroundPosition: `${50 + imageTransform.xPct}% ${50 + imageTransform.yPct}%`,
            } : {})
          }}
        />
      ) : (
        <div className="disk-placeholder">
          upload an image<br/>
          in settings ↗
        </div>
      )}

      {mode === "clockwise" && <ClockwiseCover progress={p} color={color} direction={direction} edgeStyle={edgeStyle} />}
      {mode === "sand" && <SandCover progress={p} color={color} softBoundary={softBoundary} edgeStyle={edgeStyle} />}
      {mode === "pixel" && (
        <PixelCover progress={p} color={color} gridSize={gridSize} pixelOrder={pixelOrder} />
      )}
      {(mode === "clockwise" || mode === "sand") && edgeStyle !== "crisp" && (
        <EdgeParticles
          progress={p}
          color={color}
          mode={mode}
          direction={direction}
          edgeStyle={edgeStyle}
        />
      )}
    </>
  );
}

// === Clockwise pie wedge ===
function ClockwiseCover({ progress, color, direction = "clockwise", edgeStyle = "sand" }) {
  const remaining = 1 - progress;

  // Special cases
  if (remaining <= 0.0001) return null;
  if (remaining >= 0.9999) {
    return (
      <svg className="disk-cover-svg" viewBox="0 0 100 100" preserveAspectRatio="none">
        <circle cx="50" cy="50" r="50" fill={color} />
      </svg>
    );
  }

  const cw = direction !== "counterclockwise";
  const theta = progress * 2 * Math.PI * (cw ? 1 : -1);
  // Extend the wedge to r=54 so the displacement filter can't recede past r=50.
  // We clip the entire layer to r=50, which gives us a perfect outer circle.
  const R = 54;
  const endX = 50 + R * Math.sin(theta);
  const endY = 50 - R * Math.cos(theta);
  const largeArc = remaining > 0.5 ? 1 : 0;
  const sweep = cw ? 0 : 1;
  const d = `M 50 50 L 50 ${50 - R} A ${R} ${R} 0 ${largeArc} ${sweep} ${endX.toFixed(4)} ${endY.toFixed(4)} Z`;

  const rotateDeg = progress * 360 * (cw ? 1 : -1);
  const showFx = edgeStyle !== "crisp";

  return (
    <svg className="disk-cover-svg" viewBox="0 0 100 100" preserveAspectRatio="none">
      <defs>
        {showFx && <EdgeFilter id="sand-edge" intensity={edgeStyle === "bubbles" ? 1.6 : 2.2} />}
        <CoverShadowFilter id="cover-shadow" />
        <clipPath id="disk-clip-cw"><circle cx="50" cy="50" r="50" /></clipPath>
      </defs>
      <g clipPath="url(#disk-clip-cw)">
        <path
          d={d}
          fill={color}
          filter={showFx ? "url(#sand-edge)" : "url(#cover-shadow)"}
        />
      </g>
    </svg>
  );
}

// Smoother organic edge + tiny inner shadow for depth
function EdgeFilter({ id, intensity = 2.2 }) {
  return (
    <filter
      id={id}
      filterUnits="userSpaceOnUse"
      primitiveUnits="userSpaceOnUse"
      x="-6" y="-6" width="112" height="112"
    >
      <feTurbulence type="fractalNoise" baseFrequency="0.032" numOctaves="1" seed="5" result="noise" />
      <feDisplacementMap in="SourceGraphic" in2="noise" scale={intensity} xChannelSelector="R" yChannelSelector="G" result="displaced" />
      {/* Light AA: kills pixel staircase along the displaced edge without muddying. */}
      <feGaussianBlur in="displaced" stdDeviation="0.3" result="aa" />
      <feGaussianBlur in="aa" stdDeviation="0.7" result="shadowBlur" />
      <feOffset in="shadowBlur" dx="0.3" dy="0.5" result="shadowOffset" />
      <feComponentTransfer in="shadowOffset" result="shadowAlpha">
        <feFuncA type="linear" slope="0.3" />
      </feComponentTransfer>
      <feMerge>
        <feMergeNode in="shadowAlpha" />
        <feMergeNode in="aa" />
      </feMerge>
    </filter>
  );
}

// Crisp variant still gets a faint shadow so it doesn't look pasted-on
function CoverShadowFilter({ id }) {
  return (
    <filter
      id={id}
      filterUnits="userSpaceOnUse"
      primitiveUnits="userSpaceOnUse"
      x="-6" y="-6" width="112" height="112"
    >
      <feGaussianBlur stdDeviation="0.8" result="b" />
      <feOffset in="b" dx="0.3" dy="0.5" result="o" />
      <feComponentTransfer in="o" result="a"><feFuncA type="linear" slope="0.25" /></feComponentTransfer>
      <feMerge><feMergeNode in="a" /><feMergeNode in="SourceGraphic" /></feMerge>
    </filter>
  );
}

// === Canvas2D particle simulation for sand/bubble edge effects ===
// Replaces the old per-particle CSS-animated <circle> approach.
// One canvas covers the whole disk; clipping is provided by `.disk` overflow:hidden.
function EdgeParticles({ progress, color, mode, direction, edgeStyle }) {
  const canvasRef = React.useRef(null);
  const propsRef = React.useRef({});
  propsRef.current = { progress, color, mode, direction, edgeStyle };

  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext("2d");
    const parent = canvas.parentElement;
    let raf = 0;
    let particles = [];
    let last = 0;
    let size = 0;
    let dpr = 1;

    const resize = () => {
      const rect = parent.getBoundingClientRect();
      dpr = Math.min(2, window.devicePixelRatio || 1);
      size = rect.width;
      canvas.width = Math.round(rect.width * dpr);
      canvas.height = Math.round(rect.height * dpr);
      canvas.style.width = `${rect.width}px`;
      canvas.style.height = `${rect.height}px`;
    };
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(parent);

    const spawnClockwise = (n) => {
      const { progress: pr, direction: dir } = propsRef.current;
      if (pr <= 0 || pr >= 1) return;
      const cw = dir !== "counterclockwise";
      const theta = pr * 2 * Math.PI * (cw ? 1 : -1);
      // SVG convention: 0 = up. In canvas (y down) the edge angle from +x axis is:
      const a = -Math.PI / 2 + theta;
      const ex = Math.cos(a);
      const ey = Math.sin(a);
      const sweepSign = cw ? 1 : -1;
      // Tangent points in sweep direction
      const tx = -ey * sweepSign;
      const ty = ex * sweepSign;
      const cx = (size / 2) * dpr;
      const cy = (size / 2) * dpr;
      const R = (size / 2) * dpr;
      for (let i = 0; i < n; i++) {
        // Bias spawn radius outward (more action near rim)
        const r = (0.15 + Math.pow(Math.random(), 0.5) * 0.85) * R;
        const jitter = (Math.random() - 0.5) * 2 * dpr;
        const x = cx + ex * r + tx * jitter * 0.4;
        const y = cy + ey * r + ty * jitter * 0.4;
        const tangSpeed = (10 + Math.random() * 28) * dpr;
        const radSpeed = (Math.random() * 6 - 1) * dpr;
        const vx = tx * tangSpeed + ex * radSpeed + (Math.random() - 0.5) * 4 * dpr;
        const vy = ty * tangSpeed + ey * radSpeed + (Math.random() - 0.5) * 4 * dpr;
        const tier = Math.random();
        const sz = tier < 0.45
          ? (0.4 + Math.random() * 0.5) * dpr
          : tier < 0.85
            ? (0.9 + Math.random() * 0.9) * dpr
            : (1.6 + Math.random() * 1.4) * dpr;
        particles.push({
          x, y, vx, vy,
          life: 0,
          maxLife: 0.55 + Math.random() * 0.9,
          size: sz,
          drag: 0.86 + Math.random() * 0.08,
        });
      }
    };

    const spawnSand = (n) => {
      const { progress: pr } = propsRef.current;
      if (pr <= 0 || pr >= 1) return;
      const h = (1 - pr); // 0..1 from top
      const cx = (size / 2) * dpr;
      const cy = (size / 2) * dpr;
      const R = (size / 2) * dpr;
      const yEdge = h * size * dpr;
      const dy = yEdge - cy;
      const maxX = Math.sqrt(Math.max(0, R * R - dy * dy));
      if (maxX <= 0) return;
      for (let i = 0; i < n; i++) {
        const x = cx + (Math.random() - 0.5) * 2 * maxX * 0.95;
        const y = yEdge + (Math.random() - 0.5) * 1.5 * dpr;
        const tier = Math.random();
        const sz = tier < 0.45
          ? (0.4 + Math.random() * 0.5) * dpr
          : tier < 0.85
            ? (0.9 + Math.random() * 0.9) * dpr
            : (1.6 + Math.random() * 1.3) * dpr;
        particles.push({
          x, y,
          vx: (Math.random() - 0.5) * 14 * dpr,
          vy: (4 + Math.random() * 10) * dpr,
          life: 0,
          maxLife: 0.55 + Math.random() * 0.8,
          size: sz,
          drag: 0.97,
          gravity: 48 * dpr,
        });
      }
    };

    const loop = (t) => {
      const dt = last ? Math.min(0.05, (t - last) / 1000) : 0.016;
      last = t;
      const { mode: m, color: c, edgeStyle: es } = propsRef.current;

      // Target spawn rate scales with edge style (bubbles bigger, fewer)
      const ratePerSec = m === "clockwise"
        ? (es === "bubbles" ? 120 : 280)
        : (es === "bubbles" ? 100 : 240);
      const spawn = Math.floor(ratePerSec * dt + Math.random());
      if (m === "clockwise") spawnClockwise(spawn);
      else if (m === "sand") spawnSand(spawn);

      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = c;
      const surviving = [];
      for (const p of particles) {
        p.life += dt;
        if (p.life >= p.maxLife) continue;
        p.x += p.vx * dt;
        p.y += p.vy * dt;
        if (p.gravity) p.vy += p.gravity * dt;
        p.vx *= Math.pow(p.drag, dt * 60);
        p.vy *= Math.pow(p.drag, dt * 60);
        const u = p.life / p.maxLife;
        const alpha = (1 - u) * (1 - u * 0.3);
        ctx.globalAlpha = alpha;
        const r = p.size * (es === "bubbles" ? (0.6 + u * 0.6) : (1 - u * 0.55));
        if (r <= 0.15) continue;
        ctx.beginPath();
        ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
        ctx.fill();
        if (es === "bubbles" && r > 1.2 * dpr) {
          ctx.globalAlpha = alpha * 0.7;
          ctx.fillStyle = "rgba(255,255,255,0.9)";
          ctx.beginPath();
          ctx.arc(p.x - r * 0.35, p.y - r * 0.35, r * 0.3, 0, Math.PI * 2);
          ctx.fill();
          ctx.fillStyle = c;
        }
        surviving.push(p);
      }
      particles = surviving;
      ctx.globalAlpha = 1;
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);

    return () => {
      if (raf) cancelAnimationFrame(raf);
      ro.disconnect();
    };
  }, []);

  return (
    <canvas
      ref={canvasRef}
      style={{
        position: "absolute",
        inset: 0,
        width: "100%",
        height: "100%",
        pointerEvents: "none",
      }}
    />
  );
}

// === Sand fill from bottom ===
// At p=0 disk is full; at p=1 it's empty. Cover fills the TOP portion (remaining).
function SandCover({ progress, color, softBoundary = true, edgeStyle = "sand" }) {
  const remaining = 1 - progress;
  if (remaining <= 0.0001) return null;

  // The cover occupies the top (1-p) fraction; the bottom p is "image revealed".
  // Use a clipPath rect from y=0 to y=(remaining * 100).
  const h = remaining * 100;

  return (
    <svg className="disk-cover-svg" viewBox="0 0 100 100" preserveAspectRatio="none">
      <defs>
        {edgeStyle !== "crisp" && <EdgeFilter id="sand-edge-h" intensity={edgeStyle === "bubbles" ? 1.5 : 2.0} />}
        <CoverShadowFilter id="cover-shadow-h" />
        <clipPath id="disk-clip-h"><circle cx="50" cy="50" r="50" /></clipPath>
        <clipPath id="sandClip">
          <rect x="0" y="0" width="100" height={h + 3} />
        </clipPath>
      </defs>
      <g clipPath="url(#disk-clip-h)">
        <circle
          cx="50" cy="50" r="54"
          fill={color}
          clipPath="url(#sandClip)"
          filter={edgeStyle !== "crisp" ? "url(#sand-edge-h)" : "url(#cover-shadow-h)"}
        />
        {/* Soft sand ripple at the boundary */}
        {softBoundary && h < 100 && h > 0 && (
          <path
            d={`M 0 ${h} Q 25 ${h - 1.5} 50 ${h} T 100 ${h} L 100 ${h - 0.5} L 0 ${h - 0.5} Z`}
            fill={color}
            opacity="0.6"
          />
        )}
      </g>
    </svg>
  );
}

// === Pixel tiles disappearing ===
function PixelCover({ progress, color, gridSize, pixelOrder }) {
  // pixelOrder: array of 0..N-1 shuffled, defining removal order.
  const total = gridSize * gridSize;
  const goneCount = Math.floor(progress * total);
  const goneSet = useMemo(() => {
    const s = new Set();
    const order = pixelOrder && pixelOrder.length === total ? pixelOrder : Array.from({ length: total }, (_, i) => i);
    for (let i = 0; i < goneCount; i++) s.add(order[i]);
    return s;
  }, [goneCount, pixelOrder, total]);

  return (
    <div
      className="pixel-grid"
      style={{
        gridTemplateColumns: `repeat(${gridSize}, 1fr)`,
        gridTemplateRows: `repeat(${gridSize}, 1fr)`,
      }}
    >
      {Array.from({ length: total }, (_, i) => (
        <div
          key={i}
          className={`pixel-tile ${goneSet.has(i) ? "gone" : ""}`}
          style={{ background: color }}
        />
      ))}
    </div>
  );
}

// Seeded shuffled order helper (stable for a session)
function makePixelOrder(n, seed = 42) {
  const arr = Array.from({ length: n }, (_, i) => i);
  // Simple seeded shuffle
  let s = seed;
  const rand = () => {
    s = (s * 9301 + 49297) % 233280;
    return s / 233280;
  };
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(rand() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
}

Object.assign(window, { RevealDisk, makePixelOrder });
