/* eslint-disable no-undef */
// =====================================================
// Physical pendulum sandbox — настоящий маятник + фазовый портрет
// Drag the bob → release; or click in phase plane.
// =====================================================

function PhysicalPendulumViz() {
  const [gamma, setGamma] = useState(0.0);
  const [running, setRunning] = useState(true);

  // pendulum state
  const stateRef = useRef({ y: 0.7, v: 0.0 });
  // trail of phase (y,v) positions
  const trailRef = useRef([]);
  const physRef = useRef(null);
  const phaseRef = useRef(null);
  const dragRef = useRef(null);

  // animation
  useEffect(() => {
    let raf = 0;
    let lastT = performance.now();
    let alive = true;

    const tick = (now) => {
      if (!alive) return;
      try {
      const dt = Math.min(0.04, (now - lastT) / 1000);
      lastT = now;

      if (running && !dragRef.current) {
        const F = ([y, v]) => [v, -Math.sin(y) - gamma * v];
        const newState = rk4Step((t, yy) => F(yy), 0, [stateRef.current.y, stateRef.current.v], dt);
        if (isFinite(newState[0]) && isFinite(newState[1])) {
          stateRef.current = { y: newState[0], v: newState[1] };
          // keep angle in [-3π, 3π]
          while (stateRef.current.y > 3 * Math.PI) stateRef.current.y -= 2 * Math.PI;
          while (stateRef.current.y < -3 * Math.PI) stateRef.current.y += 2 * Math.PI;
          // trail (only when running)
          trailRef.current.push([stateRef.current.y, stateRef.current.v]);
          if (trailRef.current.length > 600) trailRef.current.shift();
        }
      }

      // --- draw physical pendulum (left canvas) ---
      const cvs1 = physRef.current;
      if (cvs1) {
        const { ctx, W, H } = setupCanvas(cvs1);
        ctx.fillStyle = '#fbf6e9'; ctx.fillRect(0, 0, W, H);

        // pivot
        const cx = W / 2, cy = H * 0.32;
        const L = Math.min(W, H) * 0.36;
        const ang = stateRef.current.y;
        const bx = cx + L * Math.sin(ang);
        const by = cy + L * Math.cos(ang);

        // arc showing trajectory (last few positions)
        if (trailRef.current.length > 2) {
          ctx.save();
          ctx.strokeStyle = 'rgba(60,55,40,0.18)';
          ctx.lineWidth = 1.5;
          ctx.beginPath();
          const N = Math.min(60, trailRef.current.length);
          for (let i = 0; i < N; i++) {
            const idx = trailRef.current.length - N + i;
            const [a] = trailRef.current[idx];
            const x = cx + L * Math.sin(a);
            const y = cy + L * Math.cos(a);
            if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
          }
          ctx.stroke();
          ctx.restore();
        }

        // rod
        ctx.strokeStyle = '#564f43';
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.moveTo(cx, cy);
        ctx.lineTo(bx, by);
        ctx.stroke();
        // pivot
        ctx.fillStyle = '#1c1810';
        ctx.beginPath();
        ctx.arc(cx, cy, 5, 0, Math.PI * 2);
        ctx.fill();
        // bob — color by speed
        const speed = Math.abs(stateRef.current.v);
        const sn = Math.min(1, speed / 3.5);
        const hue = 210 - sn * 190;
        ctx.save();
        ctx.shadowColor = `hsl(${hue}, 65%, 45%)`;
        ctx.shadowBlur = 10;
        ctx.fillStyle = `hsl(${hue}, 65%, 45%)`;
        ctx.beginPath();
        ctx.arc(bx, by, 16, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
        // inner highlight
        ctx.fillStyle = 'rgba(255,255,255,0.35)';
        ctx.beginPath();
        ctx.arc(bx - 4, by - 5, 5, 0, Math.PI * 2);
        ctx.fill();

        // angle indicator
        ctx.save();
        ctx.strokeStyle = 'rgba(60,55,40,0.3)';
        ctx.setLineDash([3, 4]);
        ctx.beginPath();
        ctx.moveTo(cx, cy);
        ctx.lineTo(cx, cy + L + 8);
        ctx.stroke();
        // arc
        ctx.setLineDash([]);
        ctx.strokeStyle = 'rgba(184,88,31,0.5)';
        ctx.lineWidth = 1.8;
        ctx.beginPath();
        const sweepStart = Math.PI / 2; // bottom
        const sweepEnd = Math.PI / 2 - ang;
        ctx.arc(cx, cy, 32, Math.min(sweepStart, sweepEnd), Math.max(sweepStart, sweepEnd));
        ctx.stroke();
        ctx.restore();

        // energy bar
        const KE = 0.5 * stateRef.current.v * stateRef.current.v;
        const PE = -Math.cos(stateRef.current.y) + 1; // 0 at bottom
        const E = KE + PE;
        ctx.save();
        ctx.fillStyle = 'rgba(60,55,40,0.7)';
        ctx.font = '11px JetBrains Mono, monospace';
        ctx.textAlign = 'left';
        ctx.fillText(`E = ${E.toFixed(2)}`, 14, H - 50);
        ctx.fillText(`y = ${stateRef.current.y.toFixed(2)}`, 14, H - 36);
        ctx.fillText(`v = ${stateRef.current.v.toFixed(2)}`, 14, H - 22);
        // bar
        const barW = W - 28, barH = 6;
        ctx.fillStyle = 'rgba(60,55,40,0.15)';
        ctx.fillRect(14, H - 14, barW, barH);
        const Emax = 3.0;
        const w = Math.min(1, E / Emax) * barW;
        // gradient
        const grad = ctx.createLinearGradient(14, 0, 14 + barW, 0);
        grad.addColorStop(0, '#2a5e9e');
        grad.addColorStop(0.4, '#1f8a76');
        grad.addColorStop(0.7, '#c19614');
        grad.addColorStop(1, '#b8581f');
        ctx.fillStyle = grad;
        ctx.fillRect(14, H - 14, w, barH);
        // separatrix marker at E=2
        const sx = 14 + (2.0 / Emax) * barW;
        ctx.strokeStyle = 'rgba(168,57,44,0.7)';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        ctx.moveTo(sx, H - 16);
        ctx.lineTo(sx, H - 6);
        ctx.stroke();
        ctx.restore();
      }

      // --- draw phase portrait (right canvas) ---
      const cvs2 = phaseRef.current;
      if (cvs2) {
        const { ctx, W, H } = setupCanvas(cvs2);
        const TAU = 2 * Math.PI;
        const T = makeTransform({ xMin: -TAU, xMax: TAU, yMin: -3.5, yMax: 3.5, W, H, pad: 36 });
        drawAxes(ctx, T, {
          tickStep: Math.PI / 2,
          xLabel: 'y',
          yLabel: 'v',
          xTickFormatter: (x) => {
            const n = x / Math.PI;
            if (Math.abs(n) < 1e-3) return '0';
            if (Math.abs(n - Math.round(n)) < 1e-3) {
              const k = Math.round(n);
              if (k === 1) return 'π';
              if (k === -1) return '−π';
              return `${k}π`;
            }
            return '';
          },
        });

        // separatrix (E=1, no gamma so they pass through saddles)
        const samples = 600;
        const sepUp = [], sepDn = [];
        for (let k = 0; k <= samples; k++) {
          const y = -TAU + (k / samples) * (2 * TAU);
          const val = 2 * (1 + Math.cos(y));
          if (val >= 0) {
            const v = Math.sqrt(val);
            sepUp.push([y, v]);
            sepDn.push([y, -v]);
          }
        }
        drawPath(ctx, T, sepUp, { color: 'rgba(168,57,44,0.55)', lineWidth: 2, dashed: true });
        drawPath(ctx, T, sepDn, { color: 'rgba(168,57,44,0.55)', lineWidth: 2, dashed: true });

        // trail
        const trail = trailRef.current;
        if (trail.length > 1) {
          for (let i = 1; i < trail.length; i++) {
            const t = i / trail.length;
            const [px, py] = trail[i - 1];
            const [cx, cy] = trail[i];
            // wrap-around prevention
            if (Math.abs(cx - px) > Math.PI) continue;
            ctx.strokeStyle = `rgba(42,94,158,${t * 0.7})`;
            ctx.lineWidth = 1.5 + t * 1.5;
            ctx.beginPath();
            ctx.moveTo(T.X(px), T.Y(py));
            ctx.lineTo(T.X(cx), T.Y(cy));
            ctx.stroke();
          }
        }

        // current state dot
        const speed = Math.abs(stateRef.current.v);
        const sn = Math.min(1, speed / 3.5);
        const hue = 210 - sn * 190;
        ctx.save();
        ctx.shadowColor = `hsl(${hue}, 65%, 45%)`;
        ctx.shadowBlur = 8;
        ctx.fillStyle = `hsl(${hue}, 65%, 45%)`;
        ctx.beginPath();
        ctx.arc(T.X(stateRef.current.y), T.Y(stateRef.current.v), 6, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
        ctx.strokeStyle = '#fbf6e9';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(T.X(stateRef.current.y), T.Y(stateRef.current.v), 6, 0, Math.PI * 2);
        ctx.stroke();
      }

      raf = requestAnimationFrame(tick);
      } catch (err) { console.error('[pend tick]', err); alive = false; }
    };
    raf = requestAnimationFrame(tick);
    return () => { alive = false; cancelAnimationFrame(raf); };
  }, [gamma, running]);

  // dragging on physical pendulum
  const onPhysMouseDown = (e) => {
    const cvs = physRef.current;
    const rect = cvs.getBoundingClientRect();
    const px = e.clientX - rect.left;
    const py = e.clientY - rect.top;
    const W = rect.width, H = rect.height;
    const cx = W / 2, cy = H * 0.32;
    const L = Math.min(W, H) * 0.36;
    const bx = cx + L * Math.sin(stateRef.current.y);
    const by = cy + L * Math.cos(stateRef.current.y);
    const distToBob = Math.hypot(px - bx, py - by);
    if (distToBob < 28) {
      dragRef.current = { startTime: performance.now(), lastAng: stateRef.current.y, lastT: performance.now() };
      trailRef.current = [];
    }
  };
  const onPhysMouseMove = (e) => {
    if (!dragRef.current) return;
    const cvs = physRef.current;
    const rect = cvs.getBoundingClientRect();
    const px = e.clientX - rect.left;
    const py = e.clientY - rect.top;
    const W = rect.width, H = rect.height;
    const cx = W / 2, cy = H * 0.32;
    const dx = px - cx, dy = py - cy;
    let ang = Math.atan2(dx, dy); // 0 = straight down
    const now = performance.now();
    const dt = Math.max(0.001, (now - dragRef.current.lastT) / 1000);
    let dAng = ang - dragRef.current.lastAng;
    while (dAng > Math.PI) dAng -= 2 * Math.PI;
    while (dAng < -Math.PI) dAng += 2 * Math.PI;
    const vel = dAng / dt;
    dragRef.current.lastAng = ang;
    dragRef.current.lastT = now;
    stateRef.current = { y: ang, v: vel * 0.6 };
  };
  const onPhysMouseUp = () => {
    dragRef.current = null;
  };

  // click on phase portrait to set state
  const onPhaseClick = (e) => {
    const cvs = phaseRef.current;
    const rect = cvs.getBoundingClientRect();
    const px = e.clientX - rect.left;
    const py = e.clientY - rect.top;
    const W = rect.width, H = rect.height;
    const TAU = 2 * Math.PI;
    const T = makeTransform({ xMin: -TAU, xMax: TAU, yMin: -3.5, yMax: 3.5, W, H, pad: 36 });
    const y = T.xMin + (px - T.pad) / (W - 2 * T.pad) * (T.xMax - T.xMin);
    const v = T.yMin + ((H - py - T.pad) / (H - 2 * T.pad)) * (T.yMax - T.yMin);
    stateRef.current = { y, v };
    trailRef.current = [];
  };

  // preset launches
  const preset = (name) => {
    trailRef.current = [];
    if (name === 'small')      stateRef.current = { y: 0.5, v: 0 };
    else if (name === 'large') stateRef.current = { y: 2.5, v: 0 };
    else if (name === 'sep')   stateRef.current = { y: 0, v: 1.99 };   // separatrix
    else if (name === 'rotate')stateRef.current = { y: 0, v: 2.5 };    // rotational
    else if (name === 'stop')  stateRef.current = { y: 0, v: 0 };
  };

  return (
    <div>
      <div className="viz-wrap pendulum-twin">
        <div className="viz-header">
          Маятник — настоящая динамика и фазовый портрет в одном <span className="formula">ÿ + γẏ + sin y = 0</span>
        </div>
        <div className="pendulum-twin-grid">
          <div className="viz-canvas-host" style={{ position: 'relative' }}>
            <div style={{ position: 'absolute', top: 10, left: 14, fontFamily: 'Source Serif Pro, serif', fontSize: 13, color: 'var(--fg-mute)', pointerEvents: 'none', zIndex: 2 }}>Физика</div>
            <canvas
              ref={physRef}
              onMouseDown={onPhysMouseDown}
              onMouseMove={onPhysMouseMove}
              onMouseUp={onPhysMouseUp}
              onMouseLeave={onPhysMouseUp}
              style={{ cursor: dragRef.current ? 'grabbing' : 'grab' }}
            />
          </div>
          <div className="viz-canvas-host" style={{ position: 'relative' }}>
            <div style={{ position: 'absolute', top: 10, left: 14, fontFamily: 'Source Serif Pro, serif', fontSize: 13, color: 'var(--fg-mute)', pointerEvents: 'none', zIndex: 2 }}>Фазовая плоскость</div>
            <canvas ref={phaseRef} onClick={onPhaseClick} style={{ cursor: 'crosshair' }} />
          </div>
        </div>
        <div className="viz-controls pendulum-twin-controls">
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}>
            <div>
              <h4>Параметры</h4>
              <Slider label="Трение γ" value={gamma} setValue={setGamma} min={0} max={1.5} step={0.02} />
              <Toggle label="Время идёт" value={running} setValue={setRunning} />
            </div>
            <div>
              <h4>Запуск</h4>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
                <div className="ctl-btn" onClick={() => preset('small')}>Малые колебания</div>
                <div className="ctl-btn" onClick={() => preset('large')}>Большой размах</div>
                <div className="ctl-btn" onClick={() => preset('sep')}>Сепаратриса</div>
                <div className="ctl-btn" onClick={() => preset('rotate')}>Вращение</div>
                <div className="ctl-btn" onClick={() => preset('stop')}>Остановить</div>
                <div className="ctl-btn" onClick={() => { trailRef.current = []; }}>Стереть след</div>
              </div>
            </div>
          </div>
          <div className="ctl-note" style={{ marginTop: 14 }}>
            <b>Перетащи шар</b> на левом холсте, чтобы задать начальные условия (скорость возьмётся из скорости движения мыши).
            <b> Кликни</b> на фазовом портрете, чтобы перепрыгнуть в эту точку.
          </div>
        </div>
      </div>
      <div className="viz-caption">
        Энергетический бар внизу: синий — колебания, оранжевый — вращение. Красная риска — сепаратриса E = 2.
        На фазовом портрете пунктирная — её след: внутри неё траектории замкнуты, снаружи — раскручиваются.
      </div>
    </div>
  );
}

window.PhysicalPendulumViz = PhysicalPendulumViz;
