// Penrose v4 — Infinite Canvas.
//
// A pan/zoomable view of a procedurally-generated infinite Penrose plane.
// Tap a tile → paint it with the current palette accent. Painted tiles
// persist to localStorage. Geometry is regenerated on demand via
// PenroseGeoInf chunk cache, so persistence stays tiny.

const { useState: useCS, useEffect: useCE, useRef: useCR, useMemo: useCM, useCallback: useCCB } = React;

// Local haptic — Babel-standalone wraps each <script type="text/babel"> in
// its own scope, so app.jsx's `haptic` is not in scope here.
function cvHaptic(ms = 8) {
  if (navigator.vibrate) { try { navigator.vibrate(ms); } catch (e) {} }
}

const CANVAS_STORAGE_KEY = 'penrose-v4-canvas';

// ─── Persistence ───────────────────────────────────────────────────────

function loadCanvas() {
  try {
    const raw = localStorage.getItem(CANVAS_STORAGE_KEY);
    if (!raw) return { painted: {}, offsets: window.PenroseGeoInf.DEFAULT_OFFSETS };
    const v = JSON.parse(raw);
    return {
      painted: v.painted || {},
      offsets: (v.offsets && v.offsets.length === 5) ? v.offsets : window.PenroseGeoInf.DEFAULT_OFFSETS,
    };
  } catch (e) {
    return { painted: {}, offsets: window.PenroseGeoInf.DEFAULT_OFFSETS };
  }
}

function saveCanvas(state) {
  try { localStorage.setItem(CANVAS_STORAGE_KEY, JSON.stringify(state)); } catch (e) {}
}

// ─── The screen ────────────────────────────────────────────────────────

function CanvasScreen({ goto, daily }) {
  const C = window.PenrosePalettes.COLORS;
  const VFX = window.PenroseVFX;

  const containerRef = useCR(null);
  const cacheRef = useCR(new Map());
  const [persisted, setPersisted] = useCS(loadCanvas);
  // Camera in world units. scale = world→px multiplier.
  const [cam, setCam] = useCS({ x: 0, y: 0, scale: 22 });
  const [vp, setVp] = useCS({ w: 0, h: 0 });

  // Active brush — cycle thick / thin / glow / tappable colour, plus erase.
  const BRUSHES = useCM(() => ['thick', 'thin', 'glow', 'goldA', 'erase'], []);
  const [brushIdx, setBrushIdx] = useCS(0);
  const currentBrush = BRUSHES[brushIdx];

  // Re-render on palette change (so colours re-key).
  const [, force] = useCS(0);
  useCE(() => window.PenrosePalettes.subscribe(() => force(n => n + 1)), []);

  // Measure viewport — synchronously before first paint so the first
  // bbox query uses real dimensions (not a stale default). Use both
  // ResizeObserver (in-frame layout changes) and window resize (browser
  // viewport changes).
  React.useLayoutEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    const measure = () => {
      const r = el.getBoundingClientRect();
      if (r.width > 0 && r.height > 0) {
        setVp(prev => (prev.w === r.width && prev.h === r.height) ? prev : { w: r.width, h: r.height });
      }
    };
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    window.addEventListener('resize', measure);
    return () => { ro.disconnect(); window.removeEventListener('resize', measure); };
  }, []);

  // World-space viewport, derived from camera + screen size.
  const worldVp = useCM(() => {
    const hwx = vp.w / cam.scale / 2;
    const hwy = vp.h / cam.scale / 2;
    return {
      xmin: cam.x - hwx, xmax: cam.x + hwx,
      ymin: cam.y - hwy, ymax: cam.y + hwy,
    };
  }, [cam, vp]);

  // Tiles in viewport (chunk cache).
  const tiles = useCM(() => {
    return window.PenroseGeoInf.tilesForViewport(worldVp, cacheRef.current, {
      offsets: persisted.offsets,
    });
  }, [worldVp, persisted.offsets]);

  // ─── Pan / zoom ────────────────────────────────────────────────────
  const dragState = useCR(null);
  const pinchState = useCR(null);
  const activePointers = useCR(new Map());

  const onPointerDown = (e) => {
    e.currentTarget.setPointerCapture(e.pointerId);
    activePointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
    if (activePointers.current.size === 1) {
      dragState.current = { startX: e.clientX, startY: e.clientY, camX: cam.x, camY: cam.y, moved: false };
    } else if (activePointers.current.size === 2) {
      const pts = Array.from(activePointers.current.values());
      const dx = pts[0].x - pts[1].x, dy = pts[0].y - pts[1].y;
      pinchState.current = { dist: Math.hypot(dx, dy), startScale: cam.scale };
      dragState.current = null;
    }
  };
  const onPointerMove = (e) => {
    if (!activePointers.current.has(e.pointerId)) return;
    activePointers.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
    if (activePointers.current.size === 1 && dragState.current) {
      const dx = (e.clientX - dragState.current.startX) / cam.scale;
      const dy = (e.clientY - dragState.current.startY) / cam.scale;
      if (Math.abs(dx) + Math.abs(dy) > 0.04) dragState.current.moved = true;
      setCam(c => ({ ...c, x: dragState.current.camX - dx, y: dragState.current.camY + dy }));
    } else if (activePointers.current.size === 2 && pinchState.current) {
      const pts = Array.from(activePointers.current.values());
      const d = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
      const ratio = d / pinchState.current.dist;
      const next = Math.min(80, Math.max(8, pinchState.current.startScale * ratio));
      setCam(c => ({ ...c, scale: next }));
    }
  };
  const onPointerUp = (e) => {
    e.currentTarget.releasePointerCapture(e.pointerId);
    const wasSingle = activePointers.current.size === 1;
    const moved = dragState.current?.moved;
    activePointers.current.delete(e.pointerId);
    if (activePointers.current.size < 2) pinchState.current = null;
    if (wasSingle && !moved) {
      // tap → paint the tile under the pointer
      handleTap(e);
    }
    if (activePointers.current.size === 0) dragState.current = null;
  };

  const onWheel = (e) => {
    e.preventDefault();
    const k = Math.exp(-e.deltaY * 0.0018);
    setCam(c => ({ ...c, scale: Math.min(80, Math.max(8, c.scale * k)) }));
  };

  // Map screen → world.
  function screenToWorld(sx, sy) {
    const r = containerRef.current.getBoundingClientRect();
    const wx = (sx - r.left - r.width / 2) / cam.scale + cam.x;
    const wy = -((sy - r.top - r.height / 2) / cam.scale) + cam.y;
    return { x: wx, y: wy };
  }

  function handleTap(e) {
    const { x: wx, y: wy } = screenToWorld(e.clientX, e.clientY);
    // Find tile whose mean is closest AND that contains the point.
    let hit = null, hitD = Infinity;
    for (const t of tiles) {
      const dx = t.mean.x - wx, dy = t.mean.y - wy;
      const d = dx*dx + dy*dy;
      if (d < hitD && d < 2) {
        if (window.PenroseGeo.pointInPoly(wx, wy, t.verts)) {
          hit = t; hitD = d;
        }
      }
    }
    if (!hit) return;
    cvHaptic(6);
    setPersisted(prev => {
      const painted = { ...prev.painted };
      if (currentBrush === 'erase') {
        delete painted[hit.id];
      } else {
        painted[hit.id] = currentBrush;
      }
      const next = { ...prev, painted };
      saveCanvas(next);
      return next;
    });
  }

  // ─── Rendering ────────────────────────────────────────────────────

  const cx = vp.w / 2, cy = vp.h / 2;
  function project(p) {
    return { x: cx + (p.x - cam.x) * cam.scale, y: cy - (p.y - cam.y) * cam.scale };
  }

  function brushColor(b) {
    if (!b) return null;
    if (b === 'erase') return null;
    return C[b] || C.thick;
  }

  return (
    <div style={{ position: 'absolute', inset: 0, background: C.bg, color: C.text, overflow: 'hidden' }}>
      {/* Tiling viewport — container is the measured surface, SVG fills it. */}
      <div ref={containerRef}
           onPointerDown={onPointerDown} onPointerMove={onPointerMove}
           onPointerUp={onPointerUp} onPointerCancel={onPointerUp}
           onWheel={onWheel}
           style={{ position: 'absolute', inset: 0, touchAction: 'none', cursor: 'crosshair', userSelect: 'none' }}>
        {vp.w > 0 && (
        <svg width="100%" height="100%" viewBox={`0 0 ${vp.w} ${vp.h}`}
             preserveAspectRatio="xMidYMid slice"
             style={{ display: 'block', width: '100%', height: '100%' }}>
          {/* Background dot grid for spatial reference */}
          <defs>
            <pattern id="cv-dots" width="32" height="32" patternUnits="userSpaceOnUse">
              <circle cx="16" cy="16" r="1" fill={C.textDim} opacity="0.18" />
            </pattern>
          </defs>
          <rect x="0" y="0" width={vp.w} height={vp.h} fill="url(#cv-dots)" />

          {/* Tiles */}
          {tiles.map(t => {
            const painted = persisted.painted[t.id];
            const isThick = t.type === 'thick';
            const baseFill = isThick ? C.thick : C.thin;
            const baseEdge = isThick ? C.thickEdge : C.thinEdge;
            const fillCol = painted ? (brushColor(painted) || baseFill) : 'rgba(255,255,255,0)';
            const edgeCol = painted ? brushColor(painted) || baseEdge : C.outline;
            const path = t.verts.map((v, i) => {
              const p = project(v);
              return (i === 0 ? 'M' : 'L') + p.x + ' ' + p.y;
            }).join(' ') + ' Z';
            return (
              <path key={t.id} d={path}
                    fill={fillCol}
                    stroke={edgeCol}
                    strokeWidth={painted ? 1 : 0.5}
                    opacity={painted ? 0.95 : 1}
                    style={painted ? { ...VFX.fillStyle({ duration: 280 }) } : {}}
              />
            );
          })}
        </svg>
        )}
      </div>

      {/* Top bar */}
      <div style={{ position: 'absolute', top: 14, left: 14, right: 14, display: 'flex',
                    alignItems: 'center', justifyContent: 'space-between',
                    pointerEvents: 'none', zIndex: 5 }}>
        <div onClick={() => goto('home')} style={{
          pointerEvents: 'auto', cursor: 'pointer',
          padding: '10px 14px', borderRadius: 999,
          background: 'rgba(0,0,0,0.35)', border: '1px solid ' + C.outline,
          fontFamily: 'JetBrains Mono, monospace', fontSize: 10, letterSpacing: '0.22em', color: C.text,
        }}>← CANVAS</div>
        <div style={{ pointerEvents: 'auto', padding: '6px 10px', borderRadius: 999,
                      background: 'rgba(0,0,0,0.35)', border: '1px solid ' + C.outline,
                      fontFamily: 'JetBrains Mono, monospace', fontSize: 9, letterSpacing: '0.22em',
                      color: C.textMuted }}>
          ZOOM {Math.round(cam.scale)} · TILES {tiles.length}
        </div>
      </div>

      {/* Bottom toolbar — brush picker */}
      <div style={{ position: 'absolute', bottom: 22, left: 0, right: 0, display: 'flex',
                    justifyContent: 'center', gap: 10, zIndex: 5 }}>
        {BRUSHES.map((b, i) => {
          const isActive = i === brushIdx;
          const col = b === 'erase' ? 'transparent' : C[b];
          return (
            <div key={b} onClick={() => { setBrushIdx(i); cvHaptic(3); }}
                 style={{
                   width: 44, height: 44, borderRadius: 22,
                   background: col,
                   border: '2px solid ' + (isActive ? C.text : C.outline),
                   boxShadow: isActive ? '0 0 16px ' + (col === 'transparent' ? C.text : col) : 'none',
                   cursor: 'pointer',
                   display: 'flex', alignItems: 'center', justifyContent: 'center',
                   fontFamily: 'JetBrains Mono, monospace', fontSize: 14, color: C.text,
                   transition: 'transform 0.1s ease',
                   transform: isActive ? 'scale(1.05)' : 'scale(1)',
                 }}>
              {b === 'erase' ? '×' : ''}
            </div>
          );
        })}
      </div>

      {/* Empty-canvas hint */}
      {Object.keys(persisted.painted).length === 0 && (
        <div style={{ position: 'absolute', left: 0, right: 0, top: 100, textAlign: 'center',
                      pointerEvents: 'none', zIndex: 4 }}>
          <div style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, letterSpacing: '0.32em',
                        color: C.textMuted, ...VFX.fadeInStyle({ duration: 700 }) }}>
            TAP A TILE TO PAINT · DRAG TO PAN · PINCH TO ZOOM
          </div>
        </div>
      )}
    </div>
  );
}

window.PenroseCanvas = {
  CanvasScreen,
  loadCanvas,
  saveCanvas,
};
