"use client";

import { useEffect, type RefObject } from "react";
import { BRUSH_CONFIG } from "./config";
import { spawnStamp } from "./brushStamp";
import { createPointerPhysics, setPointerInactive, setPointerTarget, stepPointerPhysics } from "./pointerPhysics";
import { createRevealCanvas, paintRevealFrame, resizeRevealCanvas } from "./revealCanvas";
import type { BrushStamp } from "./types";

interface UseBrushRevealOptions {
  containerRef: RefObject<HTMLElement | null>;
  canvasRef: RefObject<HTMLCanvasElement | null>;
  /** "after" layer — a separately-prepared treated image, redrawn onto canvas each frame and punched with holes. The "before" layer is a plain <img> this hook never touches — it's just what shows through the holes. */
  topImageRef: RefObject<HTMLImageElement | null>;
  /** Gate so the whole effect (listeners, rAF loop) never mounts under reduced-motion or no-canvas-support. */
  enabled: boolean;
}

/**
 * The animation loop: wires pointer/touch input to `pointerPhysics`
 * (inertia — the brush trails the pointer with a slight, natural lag and a
 * touch of overshoot when it stops, not an instant snap), spawns stamps by
 * *distance traveled* rather than elapsed time (denser at low speed, sparser
 * at high speed — see BRUSH_CONFIG.stampSpacingFraction), and drives
 * `revealCanvas` once per animation frame. No React state is touched here —
 * pointer/stamp state lives in plain closure variables, updated straight
 * from native event listeners, exactly like a `useRef` would hold it.
 */
export function useBrushReveal({ containerRef, canvasRef, topImageRef, enabled }: UseBrushRevealOptions) {
  useEffect(() => {
    if (!enabled) return;

    const container = containerRef.current;
    const canvas = canvasRef.current;
    const topImage = topImageRef.current;
    if (!container || !canvas || !topImage) return;

    const reveal = createRevealCanvas(canvas);
    if (!reveal) return;

    const pool: BrushStamp[] = Array.from({ length: BRUSH_CONFIG.poolSize }, () => ({
      active: false,
      x: 0,
      y: 0,
      baseRadius: 0,
      baseScale: 1,
      opacityMultiplier: 1,
      lifetimeMultiplier: 1,
      rotation: 0,
      spawnTime: 0,
      seed: 0,
      path: null,
    }));
    let poolCursor = 0;

    function resize() {
      // clientWidth/clientHeight, not getBoundingClientRect() — the parent
      // QueueItem wrapper (see SignatureEventCard) animates this
      // container's entrance via CSS `transform: scale(0.98 → 1)`, and
      // getBoundingClientRect() reports the *visually transformed* box.
      // If this fires (initial call, ResizeObserver, or the topImage
      // "load" listener below) while that scale is still mid-flight, the
      // canvas would be baked in at a transiently-scaled size — and since
      // a transform-only change never fires ResizeObserver again, it'd
      // stay wrong until something else (e.g. a manual page refresh, by
      // luck of re-rolling the race) forced another resize. client*
      // reflects the layout box only, ignoring transforms entirely, so
      // this is correct regardless of any in-flight animation.
      resizeRevealCanvas(
        reveal!,
        { width: container!.clientWidth, height: container!.clientHeight },
        topImage!,
        BRUSH_CONFIG.dprCap,
      );
    }

    const resizeObserver = new ResizeObserver(resize);
    resizeObserver.observe(container);
    resize();
    const onImageLoad = () => resize();
    topImage.addEventListener("load", onImageLoad);

    const pointer = createPointerPhysics();
    let distanceSinceLastStamp = 0;
    let prevSmoothedX = 0;
    let prevSmoothedY = 0;

    function radiusForSpeed(speed: number): number {
      return Math.min(BRUSH_CONFIG.maxRadiusClamp, BRUSH_CONFIG.minRadius + speed * BRUSH_CONFIG.velocityRadiusMultiplier);
    }

    function spawnAt(x: number, y: number, radius: number) {
      const stamp = pool[poolCursor];
      poolCursor = (poolCursor + 1) % pool.length;
      const strokeAngle = pointer.vx !== 0 || pointer.vy !== 0 ? Math.atan2(pointer.vy, pointer.vx) : 0;
      spawnStamp(stamp, x, y, radius, strokeAngle, Math.floor(Math.random() * 1e6), performance.now());
    }

    function handlePointer(clientX: number, clientY: number) {
      const rect = container!.getBoundingClientRect();
      setPointerTarget(pointer, clientX - rect.left, clientY - rect.top);
    }

    function onMouseMove(e: MouseEvent) {
      handlePointer(e.clientX, e.clientY);
    }
    function onTouchMove(e: TouchEvent) {
      const touch = e.touches[0];
      if (touch) handlePointer(touch.clientX, touch.clientY);
    }
    function onLeave() {
      setPointerInactive(pointer);
    }

    // Passive: never blocks page scroll (mobile) or default cursor behavior (desktop).
    container.addEventListener("mousemove", onMouseMove, { passive: true });
    container.addEventListener("touchmove", onTouchMove, { passive: true });
    container.addEventListener("mouseleave", onLeave, { passive: true });
    container.addEventListener("touchend", onLeave, { passive: true });
    container.addEventListener("touchcancel", onLeave, { passive: true });

    // No-cursor devices (touch-primary, e.g. phones): nothing hovers to
    // trigger the effect passively, so a virtual pointer wanders to random
    // points on its own — same spring/spacing/stamp code path as a real
    // pointer, just fed synthetic targets instead of real events. A real
    // touch on the section still works as normal (its events fire on top).
    const isCoarsePointer = window.matchMedia("(hover: none)").matches;
    let wanderTimeoutId = 0;

    function scheduleWander() {
      const { autoWanderMinDelayMs, autoWanderMaxDelayMs } = BRUSH_CONFIG;
      const delay = autoWanderMinDelayMs + Math.random() * (autoWanderMaxDelayMs - autoWanderMinDelayMs);
      wanderTimeoutId = window.setTimeout(() => {
        if (reveal!.cssWidth > 0 && reveal!.cssHeight > 0) {
          setPointerTarget(pointer, Math.random() * reveal!.cssWidth, Math.random() * reveal!.cssHeight);
        }
        scheduleWander();
      }, delay);
    }

    if (isCoarsePointer) {
      pointer.active = true;
      scheduleWander();
    }

    let rafId = 0;

    function frame(now: number) {
      rafId = requestAnimationFrame(frame);
      if (reveal!.cssWidth === 0 || reveal!.cssHeight === 0) return;

      stepPointerPhysics(pointer);

      if (pointer.active && pointer.speed > BRUSH_CONFIG.minSpawnSpeed) {
        const dx = pointer.x - prevSmoothedX;
        const dy = pointer.y - prevSmoothedY;
        distanceSinceLastStamp += Math.sqrt(dx * dx + dy * dy);

        // Spacing derives from the radius the next stamp would have —
        // larger/faster strokes naturally space their stamps further apart,
        // smaller/slower ones stay dense, with no separate lookup needed.
        const radius = radiusForSpeed(pointer.speed);
        const spacing = radius * 2 * BRUSH_CONFIG.stampSpacingFraction;

        if (distanceSinceLastStamp >= spacing) {
          spawnAt(pointer.x, pointer.y, radius);
          distanceSinceLastStamp = 0;
        }
      }

      prevSmoothedX = pointer.x;
      prevSmoothedY = pointer.y;

      paintRevealFrame(reveal!, topImage!, pool, now, BRUSH_CONFIG.featherBlurPx);
    }

    rafId = requestAnimationFrame(frame);

    return () => {
      cancelAnimationFrame(rafId);
      window.clearTimeout(wanderTimeoutId);
      resizeObserver.disconnect();
      topImage.removeEventListener("load", onImageLoad);
      container.removeEventListener("mousemove", onMouseMove);
      container.removeEventListener("touchmove", onTouchMove);
      container.removeEventListener("mouseleave", onLeave);
      container.removeEventListener("touchend", onLeave);
      container.removeEventListener("touchcancel", onLeave);
    };
  }, [containerRef, canvasRef, topImageRef, enabled]);
}
