// pose-detect.jsx — framing-detection hooks
//
// Two hooks here:
//   useFraming({angleKey, active})  → live state about whether the subject fits the guide
//   useDeviceLevel()                 → real gyroscope reading (no mock)
//
// useFraming is currently a MOCK that progresses through the states a real
// detection pipeline would emit: looking → in frame, too far → in frame, ok →
// locked. The state shape is the wire-in surface for a real implementation.
//
// To replace the mock with real detection, swap the body of `useFraming` for one
// of:
//   • Android native: ML Kit Pose Detection in the CameraX analyzer, post detected
//     keypoints (left/right shoulder, left/right hip) into the hook via setState.
//   • PWA fallback:   TensorFlow.js MoveNet over a <video> stream, same idea.
//
// The UI never cares which backend feeds it; it just reacts to the state shape.

const { useState: useF, useEffect: useFE, useRef: useFR } = React;

// Tuneable timings for the mock progression. In a real backend these are wall-clock
// observations of detection confidence; here we fake a believable curve.
const MOCK_TO_INFRAME_MS   = 700;   // user appears in frame
const MOCK_TO_DISTANCE_MS  = 1600;  // distance corrects
const MOCK_TO_LOCK_MS      = 2400;  // pose locks
const LOCK_HOLD_TO_AUTO_MS = 1500;  // how long lock must hold before silent auto-capture badge

// Exposure thresholds, measured on average pixel luminance (0..255). Below
// TOO_DARK = too dim for a usable scan; above TOO_BRIGHT = blown highlights
// or overexposed. Sampled twice a second on a 16-pixel-wide downscaled
// snapshot of the live preview — cheap and indistinguishable from the
// luminance you'd get from a real ISP exposure meter for our purposes.
const TOO_DARK_LUMA    = 28;
const TOO_BRIGHT_LUMA  = 230;
const LUMA_SAMPLE_INTERVAL_MS = 500;
// Hard contrast detection — strong directional sun-light from one side
// produces big difference between the brightest and darkest body pixels.
// Rodin's Gen-2.5 depth estimator confuses a side-shadow with missing
// geometry, so we want to surface this before the customer captures.
// 'spread' = average luma of top 10% brightest pixels MINUS average of
// bottom 10% darkest. >TOO_HARD = contrast is too extreme.
const TOO_HARD_SPREAD  = 165;

function useFraming({ angleKey, active, videoRef }) {
  const [s, setS] = useF({
    inFrame:  false,      // subject visible in viewport
    distance: null,       // 'too_far' | 'too_close' | 'ok' | null
    locked:   false,      // subject fits guide, centered, pose stable
    lockMs:   0,          // ms the lock has been held (for auto-capture trigger)
    exposure: 'ok',       // 'too_dark' | 'too_bright' | 'too_hard' | 'ok'
    luma:     null,       // last measured avg luminance, for debugging
    spread:   null,       // last measured contrast spread, for debugging
  });

  // Reset and play the mock progression every time the angle changes.
  useFE(() => {
    if (!active) {
      setS({ inFrame: false, distance: null, locked: false, lockMs: 0, exposure: 'ok', luma: null, spread: null });
      return;
    }
    setS({ inFrame: false, distance: null, locked: false, lockMs: 0, exposure: 'ok', luma: null, spread: null });

    const t1 = setTimeout(() => setS((p) => ({ ...p, inFrame: true,  distance: 'too_far' })), MOCK_TO_INFRAME_MS);
    const t2 = setTimeout(() => setS((p) => ({ ...p, distance: 'ok' })),                       MOCK_TO_DISTANCE_MS);
    const t3 = setTimeout(() => setS((p) => ({ ...p, locked: true })),                          MOCK_TO_LOCK_MS);
    return () => [t1, t2, t3].forEach(clearTimeout);
  }, [angleKey, active]);

  // Real luminance sampling from the live video. Reads a small downscaled
  // snapshot every 500ms; if the average luminance falls outside our
  // [TOO_DARK_LUMA, TOO_BRIGHT_LUMA] band, we surface that as exposure.
  // Doesn't block the lock progression — a too-dark or too-bright lock can
  // still happen, the banner just tells the customer why the shot would be
  // unusable for the AI sculptor.
  useFE(() => {
    if (!active || !videoRef || !videoRef.current) return;
    let canceled = false;
    let canvas;  // reused across ticks
    let g;
    const sample = () => {
      if (canceled) return;
      const v = videoRef.current;
      if (!v || !v.videoWidth) return;
      try {
        if (!canvas) {
          canvas = document.createElement('canvas');
          canvas.width  = 16;
          canvas.height = Math.max(1, Math.round(16 * v.videoHeight / v.videoWidth));
          g = canvas.getContext('2d', { willReadFrequently: true });
        }
        if (!g) return;
        g.drawImage(v, 0, 0, canvas.width, canvas.height);
        const data = g.getImageData(0, 0, canvas.width, canvas.height).data;
        const lumas = [];
        let total = 0, count = 0;
        for (let i = 0; i < data.length; i += 4) {
          // Rec.709 luma weights, integer-friendly.
          const l = 0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2];
          lumas.push(l);
          total += l;
          count++;
        }
        const avg = count ? total / count : 0;
        // Contrast spread: brightest decile minus darkest decile. Catches
        // strong side-lit / backlit scenes that read 'bright on average'
        // but actually have one zone blown out and one in deep shadow —
        // the exact pattern that breaks Rodin's depth estimator.
        lumas.sort((a, b) => a - b);
        const decile = Math.max(1, Math.floor(lumas.length * 0.1));
        const dark   = lumas.slice(0, decile);
        const bright = lumas.slice(-decile);
        const darkAvg   = dark.reduce((s, x) => s + x, 0) / dark.length;
        const brightAvg = bright.reduce((s, x) => s + x, 0) / bright.length;
        const spread    = brightAvg - darkAvg;

        let exposure = 'ok';
        // Order matters: dark / bright take priority over hard contrast
        // (they're more actionable). Hard contrast surfaces when overall
        // exposure is in-range but the spread between bright and dark
        // body pixels exceeds the threshold.
        if (avg < TOO_DARK_LUMA)              exposure = 'too_dark';
        else if (avg > TOO_BRIGHT_LUMA)       exposure = 'too_bright';
        else if (spread > TOO_HARD_SPREAD)    exposure = 'too_hard';

        setS((p) =>
          (p.exposure === exposure
            && Math.abs((p.luma ?? 0) - avg) < 2
            && Math.abs((p.spread ?? 0) - spread) < 4)
            ? p
            : { ...p, exposure, luma: Math.round(avg), spread: Math.round(spread) });
      } catch (err) {
        // Cross-origin video or no frames yet — silently skip this tick.
      }
    };
    const id = setInterval(sample, LUMA_SAMPLE_INTERVAL_MS);
    sample();
    return () => { canceled = true; clearInterval(id); };
  }, [active, angleKey, videoRef]);

  // Tick lockMs upward while locked, reset to 0 when unlocked. Used by the
  // CaptureScreen to decide when to surface the silent auto-capture badge.
  useFE(() => {
    if (!s.locked) { if (s.lockMs !== 0) setS((p) => ({ ...p, lockMs: 0 })); return; }
    const start = performance && performance.now ? performance.now() : 0;
    let raf;
    const tick = (now) => {
      const ms = (performance && performance.now ? performance.now() : 0) - start;
      setS((p) => p.locked ? { ...p, lockMs: ms } : p);
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [s.locked]);

  return s;
}

// Read DeviceOrientation. Returns {levelOk, supported, tiltLR, tiltFB}.
//   beta  ≈ 90  when phone is vertical (portrait, screen facing user)
//   gamma ≈ 0   when phone is not tilted left/right
// We allow ±8° left/right and ±12° front/back before flagging.
function useDeviceLevel() {
  const [state, setState] = useF({ levelOk: true, supported: false, tiltLR: 0, tiltFB: 0 });
  const askedRef = useFR(false);

  useFE(() => {
    if (typeof window === 'undefined' || !window.DeviceOrientationEvent) {
      setState((p) => ({ ...p, supported: false }));
      return;
    }
    const handler = (e) => {
      const lr = Math.abs(e.gamma || 0);
      const fb = Math.abs((e.beta == null ? 90 : e.beta) - 90);
      setState({
        supported: true,
        tiltLR: lr, tiltFB: fb,
        levelOk: lr < 8 && fb < 12,
      });
    };

    // iOS Safari requires explicit permission. Ask once on first user gesture.
    const maybeRequest = async () => {
      if (askedRef.current) return;
      askedRef.current = true;
      if (DeviceOrientationEvent && typeof DeviceOrientationEvent.requestPermission === 'function') {
        try { await DeviceOrientationEvent.requestPermission(); } catch (e) { /* user denied, fine */ }
      }
      window.addEventListener('deviceorientation', handler, true);
    };
    // Try silently first; if events never arrive we'll wait for a tap.
    window.addEventListener('deviceorientation', handler, true);
    const gestureHandler = () => maybeRequest();
    window.addEventListener('touchstart', gestureHandler, { once: true });

    return () => {
      window.removeEventListener('deviceorientation', handler, true);
      window.removeEventListener('touchstart', gestureHandler);
    };
  }, []);

  return state;
}

Object.assign(window, { useFraming, useDeviceLevel, LOCK_HOLD_TO_AUTO_MS });
