/* eslint-disable no-undef */
// =====================================================
// Particle playground — direction-field flow
// "Кинь шарик в поток и посмотри, куда он поплывёт"
// =====================================================

const PARTICLE_EQUATIONS = {
  'x-y': {
    label: "y' = x − y",
    f: (x, y) => x - y,
    xRange: [-3, 3], yRange: [-3, 3],
  },
  'logistic': {
    label: "y' = y(1 − y)",
    f: (x, y) => y * (1 - y),
    xRange: [-3, 3], yRange: [-1.5, 2.5],
  },
  'sin': {
    label: "y' = sin x − y",
    f: (x, y) => Math.sin(x) - y,
    xRange: [-2 * Math.PI, 2 * Math.PI], yRange: [-2.5, 2.5],
  },
  'xy': {
    label: "y' = xy",
    f: (x, y) => x * y,
    xRange: [-2.5, 2.5], yRange: [-2.5, 2.5],
  },
  'cos2x': {
    label: "y' = cos(2x) − 0.3y",
    f: (x, y) => Math.cos(2 * x) - 0.3 * y,
    xRange: [-Math.PI, Math.PI], yRange: [-2, 2],
  },
};

function ParticlePlaygroundViz() {
  const [eqKey, setEqKey] = useState('x-y');
  const [showField, setShowField] = useState(true);
  const [showTrails, setShowTrails] = useState(true);
  const [colorBy, setColorBy] = useState('speed'); // 'speed' | 'slope' | 'time'
  const [particleSpeed, setParticleSpeed] = useState(1);
  const [autoShower, setAutoShower] = useState(false);

  const eq = PARTICLE_EQUATIONS[eqKey];

  // particles state stored in a ref so we don't trigger re-renders
  const particlesRef = useRef([]);
  const canvasElRef = useRef(null);
  const lastShowerRef = useRef(0);

  // reset particles on equation change
  useEffect(() => {
    particlesRef.current = [];
  }, [eqKey]);

  const addParticle = (x, y) => {
    particlesRef.current.push({
      x, y,
      trail: [[x, y]],
      age: 0,
      maxAge: 6 + Math.random() * 3, // seconds
      hue: 180 + Math.random() * 160, // for time coloring
    });
    // cap particle count
    if (particlesRef.current.length > 200) {
      particlesRef.current.splice(0, particlesRef.current.length - 200);
    }
  };

  // animation loop (always running)
  useEffect(() => {
    const cvs = canvasElRef.current;
    if (!cvs) return;
    let raf = 0;
    let lastT = performance.now();
    let alive = true;

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

      // auto shower
      if (autoShower && now - lastShowerRef.current > 200) {
        lastShowerRef.current = now;
        for (let k = 0; k < 3; k++) {
          const x = eq.xRange[0] + Math.random() * 0.15 * (eq.xRange[1] - eq.xRange[0]);
          const y = eq.yRange[0] + Math.random() * (eq.yRange[1] - eq.yRange[0]);
          addParticle(x, y);
        }
      }

      // integrate each particle: dx/dt = 1, dy/dt = f(x,y) — "downstream flow"
      const particles = particlesRef.current;
      const next = [];
      for (const p of particles) {
        const f = (t, [x, y]) => [1, eq.f(x, y)];
        const newPos = rk4Step(f, 0, [p.x, p.y], dt);
        if (!isFinite(newPos[0]) || !isFinite(newPos[1])) continue;
        p.x = newPos[0]; p.y = newPos[1];
        p.trail.push([p.x, p.y]);
        if (p.trail.length > 80) p.trail.shift();
        p.age += dt;
        // cull if out of bounds or too old
        if (p.x > eq.xRange[1] + 0.5 || p.x < eq.xRange[0] - 0.5) continue;
        if (p.y > eq.yRange[1] + 1 || p.y < eq.yRange[0] - 1) continue;
        if (p.age > p.maxAge) continue;
        next.push(p);
      }
      particlesRef.current = next;

      // redraw
      const { ctx, W, H } = setupCanvas(cvs);
      const T = makeTransform({
        xMin: eq.xRange[0], xMax: eq.xRange[1],
        yMin: eq.yRange[0], yMax: eq.yRange[1],
        W, H, pad: 36,
      });
      drawAxes(ctx, T, { tickStep: 1, xLabel: 'x', yLabel: 'y' });

      if (showField) {
        drawDirectionField(ctx, T, eq.f, {
          nx: 26, ny: 16,
          color: 'rgba(60,55,40,0.32)',
          length: 14,
        });
      }

      // particles
      for (const p of particles) {
        const fadeOut = Math.max(0, 1 - p.age / p.maxAge);
        // trail
        if (showTrails && p.trail.length > 1) {
          for (let i = 1; i < p.trail.length; i++) {
            const t = i / p.trail.length;
            const [px, py] = p.trail[i - 1];
            const [cx, cy] = p.trail[i];
            const slope = eq.f((px + cx) / 2, (py + cy) / 2);
            const speed = Math.hypot(1, slope);
            let color;
            if (colorBy === 'speed') {
              // map speed to hue (blue slow -> orange fast)
              const s = Math.min(1, (speed - 1) / 3);
              const hue = 210 - s * 180;
              color = `hsla(${hue}, 60%, 45%, ${fadeOut * t * 0.85})`;
            } else if (colorBy === 'slope') {
              // signed slope: red down, blue up
              const ang = Math.atan(slope) / (Math.PI / 2); // -1..1
              const hue = 220 - ang * 200; // 20 (red) .. 220 (blue)
              color = `hsla(${hue}, 60%, 45%, ${fadeOut * t * 0.85})`;
            } else {
              // time -> particle's own hue
              color = `hsla(${p.hue}, 60%, 45%, ${fadeOut * t * 0.85})`;
            }
            ctx.strokeStyle = color;
            ctx.lineWidth = 1.5 + 1.5 * t * fadeOut;
            ctx.beginPath();
            ctx.moveTo(T.X(px), T.Y(py));
            ctx.lineTo(T.X(cx), T.Y(cy));
            ctx.stroke();
          }
        }
        // head
        const slope = eq.f(p.x, p.y);
        const speed = Math.hypot(1, slope);
        let headColor;
        if (colorBy === 'speed') {
          const s = Math.min(1, (speed - 1) / 3);
          const hue = 210 - s * 180;
          headColor = `hsl(${hue}, 70%, 42%)`;
        } else if (colorBy === 'slope') {
          const ang = Math.atan(slope) / (Math.PI / 2);
          const hue = 220 - ang * 200;
          headColor = `hsl(${hue}, 70%, 42%)`;
        } else {
          headColor = `hsl(${p.hue}, 70%, 42%)`;
        }
        ctx.save();
        ctx.shadowColor = headColor;
        ctx.shadowBlur = 6;
        ctx.fillStyle = headColor;
        ctx.beginPath();
        ctx.arc(T.X(p.x), T.Y(p.y), 4 * fadeOut + 1, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
      }

      // (no on-canvas hint — text is below the chart)

      raf = requestAnimationFrame(tick);
      } catch (err) { console.error('[particle tick]', err); alive = false; }
    };
    raf = requestAnimationFrame(tick);
    return () => { alive = false; cancelAnimationFrame(raf); };
  }, [eqKey, showField, showTrails, colorBy, particleSpeed, autoShower]);

  const onClick = (e) => {
    const cvs = canvasElRef.current;
    if (!cvs) return;
    const rect = cvs.getBoundingClientRect();
    const px = e.clientX - rect.left;
    const py = e.clientY - rect.top;
    const W = rect.width, H = rect.height;
    const T = makeTransform({
      xMin: eq.xRange[0], xMax: eq.xRange[1],
      yMin: eq.yRange[0], yMax: eq.yRange[1],
      W, H, pad: 36,
    });
    const x = eq.xRange[0] + (px - T.pad) / (W - 2 * T.pad) * (eq.xRange[1] - eq.xRange[0]);
    const y = eq.yRange[0] + ((H - py - T.pad) / (H - 2 * T.pad)) * (eq.yRange[1] - eq.yRange[0]);
    // launch one particle per click (keep clicks discrete and obvious)
    addParticle(x, y);
  };

  const reset = () => { particlesRef.current = []; };

  return (
    <VizWrap
      title="Песочница: поток в поле направлений"
      formula={eq.label}
      captionUnder="Кликни в любую точку — оттуда выпустится шарик и поплывёт по интегральной кривой. Цвет показывает скорость или знак производной. Авто-душ сыплет шарики слева непрерывно."
      controls={
        <React.Fragment>
          <h4>Уравнение</h4>
          <ButtonGroup
            options={[
              { value: 'x-y',     label: "y′ = x − y" },
              { value: 'logistic',label: "y′ = y(1−y)" },
              { value: 'sin',     label: "y′ = sin x − y" },
              { value: 'xy',      label: "y′ = xy" },
              { value: 'cos2x',   label: "y′ = cos 2x − 0.3y" },
            ]}
            value={eqKey}
            setValue={setEqKey}
            columns={1}
          />

          <h4>Раскраска шариков</h4>
          <ButtonGroup
            options={[
              { value: 'speed', label: 'скорость' },
              { value: 'slope', label: 'знак y′' },
              { value: 'time',  label: 'случайно' },
            ]}
            value={colorBy}
            setValue={setColorBy}
            columns={1}
          />

          <h4>Скорость</h4>
          <Slider label="× темп" value={particleSpeed} setValue={setParticleSpeed} min={0.2} max={3} step={0.1} formatter={(v) => `×${v.toFixed(1)}`} />

          <h4>Поток</h4>
          <Toggle label="Поле направлений" value={showField} setValue={setShowField} />
          <Toggle label="Хвосты" value={showTrails} setValue={setShowTrails} />
          <Toggle label="Авто-душ" value={autoShower} setValue={setAutoShower} />

          <div className="ctl-btn" onClick={reset} style={{ marginTop: 12 }}>Очистить</div>
          <div className="ctl-note">Кликни по графику, чтобы бросить горсть шариков.</div>
        </React.Fragment>
      }
    >
      <canvas ref={canvasElRef} onClick={onClick} style={{ cursor: 'crosshair' }} />
    </VizWrap>
  );
}

window.ParticlePlaygroundViz = ParticlePlaygroundViz;
