// Penrose v5 — Interactive Object (Phase 0 + enrichment)
//
// Calming mosaic instrument. NOT a puzzle. Touch-reactive surface that
// rewards exploration without rules. Phase 0 scaffold + enrichment:
//
//   tap                   → ripple wave from tap point
//   drag                  → trail of mini-ripples following the finger
//   hold (480ms)          → re-seed; new mosaic builds from centre in ~300ms
//   tap near edge         → counter-ripple from opposite point (waves meet)
//   rapid taps            → multi-tap energy builds, ripples get brighter
//   no input (8s)         → idle pulse from centre (object "calls" back)
//   ambient               → tiles slowly breathe even when untouched
//   palette chip          → cycle through unlocked palettes
//   haptic                → navigator.vibrate(8) on touch tap, triple on re-seed
//
// Initial mount + every re-seed plays a fast BFS reveal animation: tiles
// cascade in from the central seed over ~300ms. No abrupt swap.
//
// Sound, swipe-morph, infinite canvas come in phases 1–2.

const { useState, useEffect, useRef, useMemo, useCallback } = React;

// ─── Constants ─────────────────────────────────────────────────────────────
const RIPPLE_STEP_MS         = 55;
const RIPPLE_PEAK_MS         = 280;
const RIPPLE_MAX_DEPTH       = 22;
const DRAG_RIPPLE_DEPTH      = 8;
const DRAG_RIPPLE_INTERVAL_MS = 70;
const DRAG_RIPPLE_MIN_DIST   = 22;
const LONG_PRESS_MS          = 480;
const LONG_PRESS_CANCEL_DIST = 30;     // px — drag farther than this cancels long-press
const TILING_RADIUS          = 64;
const FIT_MARGIN             = 0.92;
const AMBIENT_AMPLITUDE      = 0.05;
const AMBIENT_PERIOD_MS      = 5800;
const MULTITAP_BOOST         = 0.28;
const MULTITAP_DECAY_PER_S   = 0.55;
const EDGE_THRESHOLD         = 0.55;
const IDLE_MS                = 8000;
const IDLE_PULSE_INTERVAL_MS = 5200;
const BUILD_STEP_MS          = 9;      // delay between successive build rings
const BUILD_FADE_MS          = 140;    // each tile's fade-in duration

// ─── Pure: BFS depths from a seed tile ────────────────────────────────────
function bfsDepths(tiles, seedId, maxDepth) {
  const depths = new Map([[seedId, 0]]);
  const queue = [seedId];
  while (queue.length) {
    const id = queue.shift();
    const d = depths.get(id);
    if (d >= maxDepth) continue;
    for (const n of tiles[id].neighbors) {
      if (!depths.has(n)) {
        depths.set(n, d + 1);
        queue.push(n);
      }
    }
  }
  return depths;
}

function hitTile(tiles, worldX, worldY) {
  for (let i = 0; i < tiles.length; i++) {
    if (window.PenroseGeo.pointInPoly(worldX, worldY, tiles[i].verts)) return i;
  }
  return -1;
}

// ─── Component: full-screen mosaic canvas ─────────────────────────────────
function MosaicCanvas({ palette, pattern, bridge, ripples, time, multiTapEnergy, buildStartedAt }) {
  const canvasRef = useRef(null);
  const sizeRef   = useRef({ w: 0, h: 0, scale: 1, dpr: 1 });

  // Generate tiling + measure bounding radius + precompute build BFS depths.
  const { tiles, boundRadius, buildDepths } = useMemo(() => {
    const t = window.PenroseGeo.generateTiling({ radius: TILING_RADIUS, pattern });
    let r = 0;
    for (const tile of t) {
      for (const v of tile.verts) {
        const d = Math.hypot(v.x, v.y);
        if (d > r) r = d;
      }
    }
    const centre = window.PenroseGeo.findCenterTile(t);
    const depths = (centre >= 0) ? bfsDepths(t, centre, 999) : new Map();
    return { tiles: t, boundRadius: r || 1, buildDepths: depths };
  }, [pattern]);

  bridge._tiles = tiles;
  bridge._boundRadius = boundRadius;

  useEffect(() => {
    const c = canvasRef.current;
    if (!c) return;
    const resize = () => {
      const r = c.getBoundingClientRect();
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      c.width  = r.width * dpr;
      c.height = r.height * dpr;
      c.style.width  = r.width + 'px';
      c.style.height = r.height + 'px';
      const fit = (Math.min(r.width, r.height) * FIT_MARGIN) / (boundRadius * 2);
      sizeRef.current = { w: r.width, h: r.height, scale: fit, dpr };
    };
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(c);
    return () => ro.disconnect();
  }, [boundRadius]);

  // Render every frame.
  useEffect(() => {
    const c = canvasRef.current;
    if (!c || !tiles || tiles.length === 0) return;
    const { w, h, scale, dpr } = sizeRef.current;
    const ctx = c.getContext('2d');
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.fillStyle = palette.bg;
    ctx.fillRect(0, 0, w, h);

    const cx = w / 2, cy = h / 2;
    const toScreen = (x, y) => [cx + x * scale, cy - y * scale];

    const ambientPhase = (time / AMBIENT_PERIOD_MS) * Math.PI * 2;

    // Per-tile ripple energy.
    const energyMap = new Map();
    for (const ripple of ripples) {
      const dt = time - ripple.startedAt;
      const strength = ripple.strength != null ? ripple.strength : 1;
      for (const [tileId, depth] of ripple.depths) {
        const tileAt = depth * RIPPLE_STEP_MS;
        const localT = dt - tileAt;
        if (localT < 0 || localT > RIPPLE_PEAK_MS) continue;
        const n = localT / RIPPLE_PEAK_MS;
        const eased = n < 0.4 ? n / 0.4 : 1 - (n - 0.4) / 0.6;
        const v = Math.max(0, eased) * strength;
        energyMap.set(tileId, Math.min(1, (energyMap.get(tileId) || 0) + v));
      }
    }

    const sinceBuild = time - buildStartedAt;

    for (const t of tiles) {
      // ── Build-in reveal ────────────────────────────────────────────
      // Tiles cascade in from the centre over ~300ms. Skip rendering
      // until this tile's wavefront has arrived.
      const buildDepth = buildDepths.get(t.id) ?? 0;
      const tileBuildAt = buildDepth * BUILD_STEP_MS;
      const buildLocalT = sinceBuild - tileBuildAt;
      if (buildLocalT < 0) continue;
      const buildRaw = Math.min(1, buildLocalT / BUILD_FADE_MS);
      const buildProgress = 1 - Math.pow(1 - buildRaw, 3); // ease-out cubic

      // ── Steady-state energy: ripple + ambient + multi-tap baseline ─
      const rippleE = energyMap.get(t.id) || 0;
      const ambientE = AMBIENT_AMPLITUDE * (0.5 + 0.5 * Math.sin(ambientPhase + t.id * 0.073));
      const totalE = Math.min(1, rippleE + ambientE + multiTapEnergy * 0.10);

      const isThick = t.type === 'thick';
      const baseFill = isThick ? palette.thick   : palette.thin;
      const baseEdge = isThick ? palette.thickEdge : palette.thinEdge;

      // Build progress scales the tile from 0 → final size, plus ripple grow.
      const buildScale = 0.65 + 0.35 * buildProgress;
      const rippleGrow = 1 + totalE * 0.12;
      const grow = buildScale * rippleGrow;

      const tcx = t.mean.x, tcy = t.mean.y;
      ctx.beginPath();
      t.verts.forEach((v, i) => {
        const [sx, sy] = toScreen(tcx + (v.x - tcx) * grow, tcy + (v.y - tcy) * grow);
        if (i === 0) ctx.moveTo(sx, sy); else ctx.lineTo(sx, sy);
      });
      ctx.closePath();

      ctx.globalAlpha = buildProgress * (0.74 + totalE * 0.24);
      ctx.fillStyle = baseFill;
      ctx.fill();

      ctx.globalAlpha = buildProgress * (0.30 + totalE * 0.60 + multiTapEnergy * 0.10);
      ctx.lineWidth = 0.8 + totalE * 1.8;
      ctx.strokeStyle = baseEdge;
      ctx.stroke();
    }
    ctx.globalAlpha = 1;
  }, [tiles, palette, ripples, time, multiTapEnergy, buildStartedAt, buildDepths]);

  // Pointer → world conversion exposed via bridge.
  bridge._toWorld = (clientX, clientY) => {
    const c = canvasRef.current;
    const r = c.getBoundingClientRect();
    const { scale } = sizeRef.current;
    const cx = r.width / 2, cy = r.height / 2;
    const px = clientX - r.left, py = clientY - r.top;
    return { x: (px - cx) / scale, y: -(py - cy) / scale };
  };

  return <canvas ref={canvasRef} className="v5-canvas" />;
}

// ─── Main App ──────────────────────────────────────────────────────────────
function ObjectApp() {
  const daily = useMemo(() => window.PenroseDaily.getDaily(), []);
  const [pattern, setPattern] = useState(daily.pattern);
  const [seedLabel, setSeedLabel] = useState(daily.patternId);
  const [paletteId, setPaletteId] = useState(() => window.PenrosePalettes.active().id);
  const [ripples, setRipples] = useState([]);
  const [time, setTime] = useState(performance.now());
  const [multiTapEnergy, setMultiTapEnergy] = useState(0);
  const [hint, setHint] = useState(true);
  const [buildStartedAt, setBuildStartedAt] = useState(performance.now());

  const bridge = useRef({}).current;
  const lastInteractionRef = useRef(performance.now());
  const idleLastPulseRef   = useRef(0);
  const tickRef            = useRef({ lastFrameAt: performance.now() });
  const dragRef            = useRef({
    active: false,
    startX: 0, startY: 0,
    lastSpawnAt: 0, lastSpawnX: 0, lastSpawnY: 0,
    longPressId: null,
    pointerType: 'mouse',
  });

  const active = window.PenrosePalettes.getById(paletteId) || window.PenrosePalettes.active();
  const palette = active.palette;

  // Re-trigger build-in animation whenever pattern changes (or on mount).
  useEffect(() => {
    setBuildStartedAt(performance.now());
    setRipples([]); // clear any inflight ripples from the previous mosaic
  }, [pattern]);

  const spawnRipple = useCallback((tileId, opts = {}) => {
    if (tileId < 0) return;
    const tiles = bridge._tiles;
    if (!tiles) return;
    const depth = opts.depth || RIPPLE_MAX_DEPTH;
    const strength = opts.strength != null ? opts.strength : 1;
    const depths = bfsDepths(tiles, tileId, depth);
    setRipples(prev => [...prev, {
      seedId: tileId, depths, strength,
      startedAt: performance.now(),
    }]);
  }, [bridge]);

  // Tick loop — time, energy decay, idle pulse, ripple GC.
  useEffect(() => {
    let raf;
    const tick = (now) => {
      const dt = (now - tickRef.current.lastFrameAt) / 1000;
      tickRef.current.lastFrameAt = now;
      setTime(now);

      setMultiTapEnergy(e => {
        if (e < 0.001) return 0;
        return e * Math.pow(MULTITAP_DECAY_PER_S, dt);
      });

      setRipples(prev => {
        const maxAge = (RIPPLE_MAX_DEPTH + 2) * RIPPLE_STEP_MS + RIPPLE_PEAK_MS;
        const live = prev.filter(r => now - r.startedAt < maxAge);
        return live.length === prev.length ? prev : live;
      });

      // Idle pulse — only after the build-in animation has finished.
      const sinceBuild = now - buildStartedAt;
      const buildDone = sinceBuild > 700;
      const idleFor = now - lastInteractionRef.current;
      if (buildDone && idleFor > IDLE_MS && now - idleLastPulseRef.current > IDLE_PULSE_INTERVAL_MS) {
        const tiles = bridge._tiles;
        if (tiles && tiles.length) {
          const center = window.PenroseGeo.findCenterTile(tiles);
          spawnRipple(center, { strength: 0.5 });
        }
        idleLastPulseRef.current = now;
      }

      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [bridge, spawnRipple, buildStartedAt]);

  useEffect(() => {
    const t = setTimeout(() => setHint(false), 3000);
    return () => clearTimeout(t);
  }, []);

  // Helper — fire a re-seed.
  const reseed = useCallback((pointerType) => {
    const next = 0.05 + Math.random() * 0.9;
    setPattern(next);
    setSeedLabel(Math.floor(Math.random() * 0xffff).toString(16).toUpperCase().padStart(4, '0'));
    if (pointerType === 'touch' && navigator.vibrate) navigator.vibrate([8, 40, 12]);
  }, []);

  // ─── Pointer down ──────────────────────────────────────────────────────
  const onPointerDown = useCallback((e) => {
    setHint(false);
    const now = performance.now();
    lastInteractionRef.current = now;
    idleLastPulseRef.current = now;

    const tiles = bridge._tiles;
    const toWorld = bridge._toWorld;
    const boundRadius = bridge._boundRadius;
    if (!tiles || !toWorld) return;

    const w = toWorld(e.clientX, e.clientY);
    const seed = hitTile(tiles, w.x, w.y);
    // Even if tap is outside mosaic (in bg), still capture pointer for drag.

    if (e.pointerType === 'touch' && navigator.vibrate) navigator.vibrate(8);
    setMultiTapEnergy(prev => Math.min(1, prev + MULTITAP_BOOST));

    if (seed >= 0) {
      spawnRipple(seed);
      const distFromCentre = Math.hypot(w.x, w.y);
      if (boundRadius && distFromCentre > EDGE_THRESHOLD * boundRadius) {
        const counterTile = hitTile(tiles, -w.x, -w.y);
        if (counterTile >= 0 && counterTile !== seed) {
          setTimeout(() => spawnRipple(counterTile, { strength: 0.6 }), 90);
        }
      }
    }

    // Set drag state — pointermove on the same element will handle the rest.
    dragRef.current = {
      active: true,
      startX: e.clientX, startY: e.clientY,
      lastSpawnAt: now, lastSpawnX: e.clientX, lastSpawnY: e.clientY,
      longPressId: setTimeout(() => {
        reseed(e.pointerType);
        dragRef.current.longPressId = null;
      }, LONG_PRESS_MS),
      pointerType: e.pointerType,
    };

    // Capture pointer → pointermove fires even if cursor leaves the element.
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {}
  }, [bridge, spawnRipple, reseed]);

  // ─── Pointer move ──────────────────────────────────────────────────────
  // No parallax — hover is intentionally ignored. Only drag matters.
  const onPointerMove = useCallback((e) => {
    const state = dragRef.current;
    if (!state.active) return;

    lastInteractionRef.current = performance.now();
    idleLastPulseRef.current = performance.now();

    const tiles = bridge._tiles;
    const toWorld = bridge._toWorld;
    if (!tiles || !toWorld) return;

    // Cancel long-press if dragged far from start point.
    const totalMoved = Math.hypot(e.clientX - state.startX, e.clientY - state.startY);
    if (state.longPressId && totalMoved > LONG_PRESS_CANCEL_DIST) {
      clearTimeout(state.longPressId);
      state.longPressId = null;
    }

    // Drag-spawned mini-ripple along the path.
    const dx = e.clientX - state.lastSpawnX;
    const dy = e.clientY - state.lastSpawnY;
    const moved = Math.hypot(dx, dy);
    const dt = performance.now() - state.lastSpawnAt;
    if (moved > DRAG_RIPPLE_MIN_DIST && dt > DRAG_RIPPLE_INTERVAL_MS) {
      const w2 = toWorld(e.clientX, e.clientY);
      const t2 = hitTile(tiles, w2.x, w2.y);
      if (t2 >= 0) {
        spawnRipple(t2, { depth: DRAG_RIPPLE_DEPTH, strength: 0.65 });
        state.lastSpawnAt = performance.now();
        state.lastSpawnX = e.clientX;
        state.lastSpawnY = e.clientY;
      }
    }
  }, [bridge, spawnRipple]);

  // ─── Pointer up / cancel ───────────────────────────────────────────────
  const onPointerUp = useCallback((e) => {
    const state = dragRef.current;
    if (state.longPressId) {
      clearTimeout(state.longPressId);
      state.longPressId = null;
    }
    state.active = false;
    try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (_) {}
  }, []);

  // ─── Palette cycle ─────────────────────────────────────────────────────
  const cyclePalette = useCallback(() => {
    const list = window.PenrosePalettes.list.filter(p => p.free);
    const ix = list.findIndex(p => p.id === paletteId);
    const next = list[(ix + 1) % list.length];
    window.PenrosePalettes.setActive(next.id);
    setPaletteId(next.id);
    lastInteractionRef.current = performance.now();
  }, [paletteId]);

  return (
    <div
      className="v5-root"
      style={{ background: palette.bg, color: palette.text }}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={onPointerUp}
      onPointerCancel={onPointerUp}>
      <MosaicCanvas
        palette={palette}
        pattern={pattern}
        bridge={bridge}
        ripples={ripples}
        time={time}
        multiTapEnergy={multiTapEnergy}
        buildStartedAt={buildStartedAt}
      />

      <div className="v5-hud" onPointerDown={(e) => e.stopPropagation()}>
        <div className="v5-hud-left">
          <div className="v5-meta">PENROSE / TESSRA · v5 OBJECT · PHASE 0</div>
          <div className="v5-seed">SEED {seedLabel} · {daily.date}</div>
        </div>
        <button
          className="v5-palette-chip"
          onClick={cyclePalette}
          style={{ borderColor: palette.outlineStrong || 'rgba(255,255,255,.2)' }}>
          <span className="v5-swatch" style={{ background: palette.thick }} />
          <span className="v5-swatch" style={{ background: palette.thin }} />
          <span className="v5-pal-name">{active.name}</span>
        </button>
      </div>

      {multiTapEnergy > 0.05 && (
        <div className="v5-energy" aria-hidden="true">
          <div
            className="v5-energy-bar"
            style={{
              width: `${Math.round(multiTapEnergy * 100)}%`,
              background: palette.glow || palette.thin,
            }}
          />
        </div>
      )}

      {hint && (
        <div className="v5-hint" style={{ color: palette.textMuted || 'rgba(255,255,255,.55)' }}>
          tap · drag · hold to re-seed
        </div>
      )}
    </div>
  );
}

window.ObjectApp = ObjectApp;
