// retina.jsx
// RETINA SCAN — replaces the old Y/N dialog AND the standalone boot log.
// A frontal iris wireframe (procedural, canvas) is illuminated by a rotating
// radar sweep while a technical readout streams in the top-left (the boot log,
// folded in — "2-in-1"). One staged ERROR mid-scan, then on 100% the whole
// screen flashes toxic-green ("SKAN POMYŚLNY") and hard-reveals the menu.
//
// Drops in as <RetinaScan onComplete onSkip />. Total run ~9s; ESC / PRZERWIJ skips.

(function () {
  const { useEffect, useRef, useState } = React;

  // ---- timeline keyframes (seconds) ----
  const T = {
    error:    3.9,   // staged failure
    recalib:  4.7,   // recovery
    complete: 7.9,   // reaches 100% → success flash
    end:      9.6,   // hand off to menu (longer hold so the verdict lands)
  };

  // status line script: [time, text, phase]
  const STATUS = [
    [0.0,  'inicjalizacja skanera siatkówki…',     'scan'],
    [1.2,  'mapowanie naczyń włosowatych…',        'scan'],
    [2.5,  'dopasowanie wzorca tęczówki…',         'scan'],
    [T.error,   '[ERR] niezgodność wzorca · 0x4F', 'error'],
    [T.recalib, 'rekalibracja sygnału…',           'scan'],
    [6.1,  'weryfikacja biometryczna…',            'scan'],
    [T.complete, 'skan pomyślny',                  'success'],
  ];

  // streamed readout (folded-in boot log). kind: '', 'warn', 'ok'
  const LOG = [
    [0.15, 'ÐYS.MAINFRAME // auth',            ''],
    [0.6,  'mount retina.driver ······· OK',   'ok'],
    [1.2,  'biometric.handshake ······· OK',   'ok'],
    [1.9,  'narrative.core ············ OK',   'ok'],
    [2.7,  'identity.module ··········· ····', ''],
    [T.error,   '[WARN] szum sygnału',         'warn'],
    [4.9,  'rekalibracja ·············· OK',   'ok'],
    [6.0,  'identity.module ··········· OK',   'ok'],
    [T.complete, 'operator.rozpoznany ······· OK', 'ok'],
  ];

  function hexToRgb(h) {
    const m = (h || '').trim().replace('#', '');
    if (m.length === 3) return [parseInt(m[0]+m[0],16), parseInt(m[1]+m[1],16), parseInt(m[2]+m[2],16)];
    return [parseInt(m.slice(0,2),16) || 169, parseInt(m.slice(2,4),16) || 0, parseInt(m.slice(4,6),16) || 0];
  }

  function RetinaScan({ onComplete, onSkip }) {
    const { t } = useLang();
    const STATUS_TXT = t('scan_status');   // localized status line texts (len 7)
    const LOG_TXT = t('scan_log');          // localized streamed log texts (len 9)
    const cvsRef = useRef(null);
    const [pct, setPct] = useState(0);
    const [phase, setPhase] = useState('scan');     // scan | error | success
    const [status, setStatus] = useState(STATUS_TXT[0]);
    const [logs, setLogs] = useState([]);
    const doneRef = useRef(false);

    function bye(cb) {
      if (doneRef.current) return;
      doneRef.current = true;
      cb && cb();
    }
    function skip() {
      bye(() => { if (window.Audio && window.Audio.isEnabled()) window.Audio.woosh(); onSkip(); });
    }

    // ESC skips
    useEffect(() => {
      function onKey(e) { if (e.key === 'Escape') { e.preventDefault(); skip(); } }
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, []);

    // ---- single rAF: drives canvas + throttled React state + timeline events ----
    useEffect(() => {
      const cvs = cvsRef.current;
      const ctx = cvs.getContext('2d');
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      let W = 0, H = 0;
      function resize() {
        const r = cvs.getBoundingClientRect();
        W = r.width; H = r.height;
        cvs.width = W * dpr; cvs.height = H * dpr;
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      }
      resize();
      const ro = new ResizeObserver(resize);
      ro.observe(cvs);
      cvs.style.cursor = 'none';
      document.body.classList.add('is-scanning');
      // hide ambient waveform during auth (focal stays on the iris)
      const gw = document.querySelector('#ghost-wave');
      const gwPrevDisplay = gw ? gw.style.display : null;
      if (gw) gw.style.display = 'none';

      const baseRed = getComputedStyle(document.documentElement).getPropertyValue('--red');
      const [R, G, B] = hexToRgb(baseRed);
      const col = (a) => `rgba(${R},${G},${B},${a})`;
      const hot = (a) => `rgba(255,${Math.min(255, G + 90)},${Math.min(255, B + 70)},${a})`;

      // ---- procedural 3D eyeball (wireframe sphere + iris + pupil) ----
      function buildEye() {
        const verts = [], edges = [];
        const V = (x, y, z) => { verts.push([x, y, z]); return verts.length - 1; };
        const E = (a, b) => edges.push([a, b]);
        const LAT = 12, LON = 26;   // +30% mesh density on both azimuths
        const ring = [];
        for (let i = 0; i <= LAT; i++) {
          const phi = (i / LAT) * Math.PI;
          const y = Math.cos(phi), r = Math.sin(phi);
          const row = [];
          for (let j = 0; j < LON; j++) {
            const th = (j / LON) * Math.PI * 2;
            row.push(V(r * Math.cos(th), y, r * Math.sin(th)));
          }
          ring.push(row);
        }
        for (let i = 0; i <= LAT; i++) for (let j = 0; j < LON; j++) E(ring[i][j], ring[i][(j + 1) % LON]);
        for (let i = 0; i < LAT; i++) for (let j = 0; j < LON; j++) E(ring[i][j], ring[i + 1][j]);
        // iris + pupil rings sit on the front pole (+z), drawn separately (not in `edges`)
        const IRN = 32, irisZ = 0.94, irisR = 0.42, pupZ = 0.99, pupR = 0.17;
        const irisIdx = [], pupIdx = [];
        for (let k = 0; k < IRN; k++) {
          const a = (k / IRN) * Math.PI * 2;
          irisIdx.push(V(Math.cos(a) * irisR, Math.sin(a) * irisR, irisZ));
        }
        const irisCenter = V(0, 0, 1.02);
        for (let k = 0; k < IRN; k++) { const a = (k / IRN) * Math.PI * 2; pupIdx.push(V(Math.cos(a) * pupR, Math.sin(a) * pupR, pupZ)); }
        // radial iris fibres (promienisty ślad tęczówki): striae from pupil rim to
        // iris edge, length-jittered, each tagged with its base angle so a rotating
        // sweep can light them up into a radiant trace.
        const FIB = 56, fiberIdx = [];
        for (let k = 0; k < FIB; k++) {
          const a = (k / FIB) * Math.PI * 2 + (Math.random() - 0.5) * 0.05;
          const rIn = pupR * (1.04 + Math.random() * 0.05);
          const rOut = irisR * (0.9 + Math.random() * 0.1);
          const zIn = pupZ - 0.012, zOut = irisZ + 0.006;
          const inV = V(Math.cos(a) * rIn, Math.sin(a) * rIn, zIn);
          const outV = V(Math.cos(a) * rOut, Math.sin(a) * rOut, zOut);
          fiberIdx.push([inV, outV, a, Math.random()]);
        }
        return { verts, edges, irisIdx, pupIdx, irisCenter, fiberIdx, IRN };
      }
      function projectEye(v, yaw, pitch) {
        const [x, y, z] = v;
        const cp = Math.cos(pitch), sp = Math.sin(pitch);
        const y1 = y * cp - z * sp, z1 = y * sp + z * cp;
        const cyw = Math.cos(yaw), syw = Math.sin(yaw);
        const x2 = x * cyw + z1 * syw, z2 = -x * syw + z1 * cyw;
        const dist = 3.0, f = 1.75 / (dist + z2);
        return { x: x2 * f, y: -y1 * f, z: z2 };
      }
      const eye = buildEye();

      const audioOK = () => window.Audio && window.Audio.isEnabled();
      const t0 = performance.now();
      let elapsed = 0;            // accumulates real seconds / speed (no rewind on speed change)
      let lastNow = t0;
      // debug seek: frame applies window.__scanSeekTo if set (capture only)
      let progress = 0;          // smoothed 0..100 for canvas
      let statusIdx = 0, logIdx = 0;
      let lastPct = -1;
      let raf = 0;
      // saccade state — fast natural eye darting
      let gazeYaw = 0, gazePitch = 0, tgtYaw = 0, tgtPitch = 0, nextSaccade = 0;

      function targetProgress(el) {
        if (el < T.error)    return (el / T.error) * 58;
        if (el < T.recalib)  return 58 - ((el - T.error) / (T.recalib - T.error)) * 13; // dip to 45
        if (el < T.complete) return 45 + ((el - T.recalib) / (T.complete - T.recalib)) * 55;
        return 100;
      }

      function frame(now) {
        const dt = (now - lastNow) / 1000;
        lastNow = now;
        elapsed += dt / (window.__scanSpeed || 1);
        if (window.__scanSeekTo != null) { elapsed = window.__scanSeekTo; window.__scanSeekTo = null; }
        const el = elapsed;

        // fire status events
        while (statusIdx < STATUS.length && el >= STATUS[statusIdx][0]) {
          const ph = STATUS[statusIdx][2];
          setStatus(STATUS_TXT[statusIdx]); setPhase(ph);
          if (ph === 'error' && audioOK()) window.Audio.blip(165);
          if (ph === 'success' && audioOK()) { window.Audio.woosh(); window.Audio.blip(920); }
          if (ph === 'scan' && statusIdx > 0 && audioOK()) window.Audio.blip(520);
          statusIdx++;
        }
        // fire log events
        while (logIdx < LOG.length && el >= LOG[logIdx][0]) {
          const entry = LOG[logIdx];
          setLogs((L) => [...L, { text: LOG_TXT[logIdx], kind: entry[2] }]);
          if (audioOK()) window.Audio.tick();
          logIdx++;
        }

        // progress
        const tp = targetProgress(el);
        progress += (tp - progress) * 0.12;
        const shown = Math.round(progress);
        if (shown !== lastPct) { lastPct = shown; setPct(shown); }

        // error intensity (jitter) — peaks right after T.error, decays before recalib
        let err = 0;
        if (el >= T.error && el < T.recalib) {
          err = Math.max(0, 1 - (el - T.error) / (T.recalib - T.error));
        }

        // ---- draw 3D eyeball ----
        ctx.clearRect(0, 0, W, H);
        const cx = W / 2, cy = H / 2;
        const maxR = Math.min(W, H) * 0.30;                 // ~70% of the old iris
        const fade = Math.min(1, el / 1.3);                 // overall draw-in

        // SACCADES — natural fast eye darting: hold a brief fixation, then snap
        // to a new target across the surface. Error state piles shudder on top.
        if (el >= nextSaccade) {
          const big = Math.random() < 0.55;
          const ry = big ? 0.36 : 0.13;
          const rp = big ? 0.22 : 0.09;
          tgtYaw = (Math.random() * 2 - 1) * ry;
          tgtPitch = (Math.random() * 2 - 1) * rp;
          nextSaccade = el + 0.14 + Math.random() * 0.48; // fixation ~140–620ms
        }
        // snap toward target fast (settles in a few frames), micro-tremor while fixating
        gazeYaw += (tgtYaw - gazeYaw) * 0.45;
        gazePitch += (tgtPitch - gazePitch) * 0.45;
        const tremor = 0.004;
        const yaw = gazeYaw + Math.sin(el * 47) * tremor + (err ? (Math.random() - 0.5) * 0.28 : 0);
        const pitch = gazePitch + Math.cos(el * 41) * tremor + (err ? (Math.random() - 0.5) * 0.17 : 0);

        const jx = err ? (Math.random() - 0.5) * 9 * err : 0;
        const jy = err ? (Math.random() - 0.5) * 6 * err : 0;

        ctx.save();
        ctx.translate(cx + jx, cy + jy);

        const beamY = -maxR + ((el * 0.5) % 1) * maxR * 2;  // scan beam y (local)
        const P = eye.verts.map(v => projectEye(v, yaw, pitch));

        // sphere wireframe — front bright, back faint, beam-lit edges flare hot
        for (let i = 0; i < eye.edges.length; i++) {
          const [a, b] = eye.edges[i];
          const pa = P[a], pb = P[b];
          const az = (pa.z + pb.z) / 2;
          const front = az > 0;
          const depth = Math.max(0, Math.min(1, (az + 1) / 2));
          const alpha = (front ? 0.24 + depth * 0.5 : 0.08 + depth * 0.12) * fade;
          const my = ((pa.y + pb.y) / 2) * maxR;
          const near = Math.max(0, 1 - Math.abs(my - beamY) / 24);
          ctx.strokeStyle = near > 0.2 ? hot(Math.min(1, alpha + near * 0.6)) : col(alpha);
          ctx.lineWidth = front ? 1.0 : 0.6;
          ctx.beginPath();
          ctx.moveTo(pa.x * maxR, pa.y * maxR);
          ctx.lineTo(pb.x * maxR, pb.y * maxR);
          ctx.stroke();
        }

        // iris disc — ring + spokes to centre
        const pc = projectEye(eye.verts[eye.irisCenter], yaw, pitch);
        const irisPts = eye.irisIdx.map(idx => projectEye(eye.verts[idx], yaw, pitch));
        ctx.strokeStyle = hot(0.8 * fade);
        ctx.lineWidth = 1.4;
        ctx.beginPath();
        irisPts.forEach((p, k) => { const X = p.x * maxR, Y = p.y * maxR; k ? ctx.lineTo(X, Y) : ctx.moveTo(X, Y); });
        ctx.closePath(); ctx.stroke();
        ctx.strokeStyle = col(0.4 * fade);
        ctx.lineWidth = 0.7;
        irisPts.forEach((p, k) => {
          if (k % 2) return;
          ctx.beginPath(); ctx.moveTo(pc.x * maxR, pc.y * maxR); ctx.lineTo(p.x * maxR, p.y * maxR); ctx.stroke();
        });

        // radial iris fibres + a rotating radiant trace lighting them up
        const sweepA = (el * 1.5) % (Math.PI * 2);
        for (let f = 0; f < eye.fiberIdx.length; f++) {
          const [ai, bi, fa, rnd] = eye.fiberIdx[f];
          const pa = projectEye(eye.verts[ai], yaw, pitch);
          const pb = projectEye(eye.verts[bi], yaw, pitch);
          if ((pa.z + pb.z) / 2 < 0.15) continue; // front hemisphere only
          let da = Math.abs(fa - sweepA); da = Math.min(da, Math.PI * 2 - da);
          const near = Math.max(0, 1 - da / 0.6);
          const baseA = 0.1 + rnd * 0.14;
          if (near > 0.05) {
            ctx.strokeStyle = hot((baseA + near * 0.8) * fade);
            ctx.lineWidth = 0.6 + near * 0.9;
          } else {
            ctx.strokeStyle = col(baseA * fade);
            ctx.lineWidth = 0.55;
          }
          ctx.beginPath();
          ctx.moveTo(pa.x * maxR, pa.y * maxR);
          ctx.lineTo(pb.x * maxR, pb.y * maxR);
          ctx.stroke();
        }

        // pupil — dark fill + hot rim, dilates on error
        const pupPts = eye.pupIdx.map(idx => projectEye(eye.verts[idx], yaw, pitch));
        let pr = 0; pupPts.forEach(p => { pr += Math.hypot(p.x - pc.x, p.y - pc.y); });
        pr = (pr / pupPts.length) * maxR * (1 + err * 0.5 + Math.sin(el * 2.4) * 0.02);
        const pcx = pc.x * maxR, pcy = pc.y * maxR;
        ctx.fillStyle = 'rgba(6,6,6,0.95)';
        ctx.beginPath(); ctx.arc(pcx, pcy, pr, 0, Math.PI * 2); ctx.fill();
        ctx.strokeStyle = hot((0.75 + err * 0.25) * fade);
        ctx.lineWidth = 1.6;
        ctx.beginPath(); ctx.arc(pcx, pcy, pr, 0, Math.PI * 2); ctx.stroke();
        ctx.fillStyle = hot(0.55 * fade);
        ctx.beginPath(); ctx.arc(pcx - pr * 0.32, pcy - pr * 0.34, 1.8, 0, Math.PI * 2); ctx.fill();

        // scan beam across the eye
        const bg = ctx.createLinearGradient(0, beamY - 12, 0, beamY + 12);
        bg.addColorStop(0, hot(0)); bg.addColorStop(0.5, hot(0.22 * fade)); bg.addColorStop(1, hot(0));
        ctx.fillStyle = bg;
        ctx.fillRect(-maxR * 1.05, beamY - 12, maxR * 2.1, 24);

        ctx.restore();

        // reticle brackets + crosshair
        const fw = maxR * 2.5, fx = cx - fw / 2, fy = cy - fw / 2, c = 16;
        ctx.strokeStyle = col(0.4); ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(fx, fy + c); ctx.lineTo(fx, fy); ctx.lineTo(fx + c, fy);
        ctx.moveTo(fx + fw - c, fy); ctx.lineTo(fx + fw, fy); ctx.lineTo(fx + fw, fy + c);
        ctx.moveTo(fx + fw, fy + fw - c); ctx.lineTo(fx + fw, fy + fw); ctx.lineTo(fx + fw - c, fy + fw);
        ctx.moveTo(fx + c, fy + fw); ctx.lineTo(fx, fy + fw); ctx.lineTo(fx, fy + fw - c);
        ctx.stroke();
        // crosshair ticks
        ctx.strokeStyle = col(0.25);
        ctx.beginPath();
        ctx.moveTo(cx, fy); ctx.lineTo(cx, fy + 10);
        ctx.moveTo(cx, fy + fw); ctx.lineTo(cx, fy + fw - 10);
        ctx.moveTo(fx, cy); ctx.lineTo(fx + 10, cy);
        ctx.moveTo(fx + fw, cy); ctx.lineTo(fx + fw - 10, cy);
        ctx.stroke();
        // labels
        ctx.fillStyle = col(0.5);
        ctx.font = '9px "Nippo", monospace';
        ctx.fillText('RETINA.SCAN', fx + 2, fy - 7);
        ctx.fillText('0x4F', fx + fw - 26, fy - 7);
        ctx.fillText(err ? 'SIG//LOSS' : 'TRACKING', fx + 2, fy + fw + 14);

        raf = requestAnimationFrame(frame);

        // hand off after success hold
        if (el >= T.end) {
          cancelAnimationFrame(raf);
          bye(() => onComplete());
        }
      }
      raf = requestAnimationFrame(frame);

      return () => { cancelAnimationFrame(raf); ro.disconnect(); document.body.classList.remove('is-scanning'); if (gw) gw.style.display = gwPrevDisplay || ''; };
    }, []);

    return (
      <div className={`scan scan--${phase}`}>
        <div className="scan__log">
          {logs.map((l, i) => (
            <div className={`scan__logline ${l.kind ? 'is-' + l.kind : ''}`} key={i}>{l.text}</div>
          ))}
        </div>

        <div className="scan__stage">
          <canvas ref={cvsRef} className="scan__iris"></canvas>
        </div>

        <div className="scan__hud">
          <div className="scan__status">{status}</div>
          <div className="scan__bar">
            <div className="scan__bar-fill" style={{ width: pct + '%' }}></div>
          </div>
          <div className="scan__pct">{String(pct).padStart(3, '0')}%</div>
        </div>

        <button className="scan__abort" onClick={skip}
                onMouseEnter={() => window.Audio && window.Audio.tick()}>
          {t('scan_abort')}
        </button>

        <div className="scan__flash" aria-hidden="true"></div>
        <div className="scan__verdict" aria-hidden="true">
          <span className="scan__verdict-main">{t('scan_verdict_main')}</span>
          <span className="scan__verdict-sub">{t('scan_verdict_sub')}</span>
        </div>
      </div>
    );
  }

  Object.assign(window, { RetinaScan });
})();
