'use strict';

/**
 * ReactionSim — Chemical reaction kinetics simulation.
 * Particle-based A + B <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> C (and variants) with Arrhenius kinetics.
 * Renders: glowing molecules, flash effects on reaction,
 *          live concentration graph, energy profile diagram.
 */
class ReactionSim {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx    = canvas.getContext('2d');
    this.W = 0; this.H = 0;

    this.particles  = [];
    this.flashes    = [];    // [{x, y, t, maxT, color}]
    this._history   = [];    // [{step, nA, nB, nC}]
    this._nextId    = 0;

    // Parameters
    this.N  = 28;            // initial molecules per reactive species
    this.T  = 1.2;           // temperature 0.2–4.0
    this.Ea = 2.0;           // activation energy 0.5–5.0
    this.mode = 'forward';   // 'forward' | 'reversible' | 'chain'
    this.reactionOn = true;

    // Runtime stats
    this._steps           = 0;
    this._totalReactions  = 0;
    this._recentReactions = 0;
    this._rate            = 0;  // reactions per step (ema)

    this._raf  = null;
    this._dpr  = 1;
    this.onUpdate = null;

    // Spatial grid
    this._grid     = new Map();
    this._GRID_C   = 22;  // cell size (> max particle diameter)
  }

  /* ────────────────────────── Lifecycle ────────────────────────── */

  fit() {
    const dpr = window.devicePixelRatio || 1;
    this._dpr = dpr;
    const w = this.canvas.offsetWidth;
    const h = this.canvas.offsetHeight;
    this.canvas.width  = w * dpr;
    this.canvas.height = h * dpr;
    this.W = w; this.H = h;
    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    this.reset();
  }

  reset() {
    const { W, H } = this;
    if (!W || !H) return;
    this.particles  = [];
    this.flashes    = [];
    this._history   = [];
    this._steps     = 0;
    this._totalReactions  = 0;
    this._recentReactions = 0;
    this._rate      = 0;
    this._nextId    = 0;

    // Spawn N of A and N of B
    this._spawnType('A', this.N);
    this._spawnType('B', this.N);
    this._recordHistory();
  }

  _spawnType(type, count) {
    const { W, H } = this;
    const r = this._radius(type);
    const margin = 12;
    let placed = 0, attempts = 0;
    while (placed < count && attempts < count * 60) {
      attempts++;
      const x = margin + r + Math.random() * (W - 2 * r - margin * 2);
      const y = margin + r + Math.random() * (H - 2 * r - margin * 2);
      let overlap = false;
      for (const p of this.particles) {
        const dx = p.x - x, dy = p.y - y;
        if (dx * dx + dy * dy < (p.r + r + 1) ** 2) { overlap = true; break; }
      }
      if (overlap) continue;
      const ang = Math.random() * Math.PI * 2;
      const spd = this._baseSpeed(type) * (0.6 + Math.random() * 0.8);
      this.particles.push({ x, y, vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd, r, type, id: this._nextId++ });
      placed++;
    }
  }

  start() {
    if (this._raf) return;
    this._lastTs = performance.now();
    const loop = (ts) => {
      this._raf = requestAnimationFrame(loop);
      const dt = Math.min((ts - this._lastTs) / 1000, 0.05);
      this._lastTs = ts;
      if (window.LabFX) LabFX.particles.update(dt);
      for (let i = 0; i < 3; i++) this._step();
      this.draw();
    };
    this._raf = requestAnimationFrame(loop);
  }

  stop() {
    if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; }
  }

  /* ────────────────────────── Parameters ────────────────────────── */

  setN(n) {
    this.N = Math.max(5, Math.min(80, n));
    this.reset();
  }

  setT(t) {
    const ratio = Math.max(0.1, t) / Math.max(0.1, this.T);
    this.T = Math.max(0.2, Math.min(4.0, t));
    const scale = Math.sqrt(ratio);
    for (const p of this.particles) { p.vx *= scale; p.vy *= scale; }
  }

  setEa(ea) {
    this.Ea = Math.max(0.5, Math.min(5.0, ea));
  }

  setMode(mode) { this.mode = mode; }

  toggleReaction() { this.reactionOn = !this.reactionOn; }

  preset(name) {
    this.reactionOn = true;
    const presets = {
      simple:     { N: 28, T: 1.2, Ea: 1.8, mode: 'forward'    },
      reversible: { N: 22, T: 1.5, Ea: 1.5, mode: 'reversible' },
      hot:        { N: 25, T: 2.8, Ea: 2.0, mode: 'forward'    },
      cold:       { N: 25, T: 0.4, Ea: 1.5, mode: 'forward'    },
      chain:      { N: 18, T: 1.8, Ea: 0.9, mode: 'chain'      },
    };
    Object.assign(this, presets[name] || {});
    this.reset();
  }

  info() {
    let nA = 0, nB = 0, nC = 0;
    for (const p of this.particles) {
      if (p.type === 'A') nA++;
      else if (p.type === 'B') nB++;
      else nC++;
    }
    return { nA, nB, nC, total: this.particles.length, reactions: this._totalReactions, rate: this._rate };
  }

  /* ────────────────────────── Helpers ────────────────────────── */

  _radius(type)    { return type === 'C' ? 7 : 5; }
  _baseSpeed(type) { return (type === 'C' ? 0.55 : 1.0) * this.T * 3.2; }
  _color(type)     { return { A: '#06D6E0', B: '#EF476F', C: '#FFD166' }[type] || '#aaa'; }

  /* ────────────────────────── Physics ────────────────────────── */

  _buildGrid() {
    this._grid.clear();
    const cs = this._GRID_C;
    for (const p of this.particles) {
      const key = `${Math.floor(p.x / cs)},${Math.floor(p.y / cs)}`;
      if (!this._grid.has(key)) this._grid.set(key, []);
      this._grid.get(key).push(p);
    }
  }

  _neighbors(p) {
    const cs = this._GRID_C;
    const gx = Math.floor(p.x / cs), gy = Math.floor(p.y / cs);
    const out = [];
    for (let dx = -1; dx <= 1; dx++)
      for (let dy = -1; dy <= 1; dy++) {
        const cell = this._grid.get(`${gx + dx},${gy + dy}`);
        if (cell) for (const q of cell) if (q !== p) out.push(q);
      }
    return out;
  }

  _step() {
    const { W, H } = this;
    const dt = 0.55;

    // Move + wall bounce
    for (const p of this.particles) {
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      if (p.x < p.r)     { p.x = p.r;     p.vx =  Math.abs(p.vx); }
      if (p.x > W - p.r) { p.x = W - p.r; p.vx = -Math.abs(p.vx); }
      if (p.y < p.r)     { p.y = p.r;     p.vy =  Math.abs(p.vy); }
      if (p.y > H - p.r) { p.y = H - p.r; p.vy = -Math.abs(p.vy); }
    }

    this._buildGrid();

    const toRemove = new Set();
    const toAdd    = [];

    // Pairwise: collision detection, reaction check, elastic bounce
    for (const p of this.particles) {
      if (toRemove.has(p.id)) continue;
      for (const q of this._neighbors(p)) {
        if (q.id <= p.id || toRemove.has(q.id)) continue;

        const dx = q.x - p.x, dy = q.y - p.y;
        const dist2 = dx * dx + dy * dy;
        const minD  = p.r + q.r;
        if (dist2 >= minD * minD) continue;

        const dist = Math.sqrt(dist2);

        // Try chemical reaction
        if (this.reactionOn && this._tryReact(p, q, dx, dy, dist, toRemove, toAdd)) continue;

        // Elastic collision
        const nx = dx / dist, ny = dy / dist;
        const dvx = p.vx - q.vx, dvy = p.vy - q.vy;
        const dot = dvx * nx + dvy * ny;
        if (dot >= 0) {
          // Just separate overlapping particles that are already moving apart
          const ov = (minD - dist) * 0.5;
          p.x -= nx * ov; p.y -= ny * ov;
          q.x += nx * ov; q.y += ny * ov;
          continue;
        }
        const m1 = p.r * p.r, m2 = q.r * q.r;
        const imp = (2 * dot) / (m1 + m2);
        p.vx -= imp * m2 * nx; p.vy -= imp * m2 * ny;
        q.vx += imp * m1 * nx; q.vy += imp * m1 * ny;
        const ov = (minD - dist) * 0.5;
        p.x -= nx * ov; p.y -= ny * ov;
        q.x += nx * ov; q.y += ny * ov;
      }
    }

    // Spontaneous decomposition C <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> A + B (reversible mode)
    if (this.mode === 'reversible') {
      const prob = 0.00022 * this.T * Math.exp(-this.Ea * 0.38 / this.T);
      for (const p of this.particles) {
        if (p.type !== 'C' || toRemove.has(p.id)) continue;
        if (Math.random() < prob) {
          toRemove.add(p.id);
          const ang = Math.random() * Math.PI * 2;
          const spd = this._baseSpeed('A');
          const mk = id => ({ x: p.x + Math.cos(ang + id * Math.PI) * 5,
                               y: p.y + Math.sin(ang + id * Math.PI) * 5,
                               vx: Math.cos(ang + id * Math.PI) * spd * (0.7 + Math.random() * 0.6),
                               vy: Math.sin(ang + id * Math.PI) * spd * (0.7 + Math.random() * 0.6),
                               r: 5, type: id === 0 ? 'A' : 'B', id: this._nextId++ });
          toAdd.push(mk(0), mk(1));
          this.flashes.push({ x: p.x, y: p.y, t: 0, maxT: 14, color: '100,160,255' });
        }
      }
    }

    // Apply changes
    if (toRemove.size) this.particles = this.particles.filter(p => !toRemove.has(p.id));
    for (const p of toAdd) this.particles.push(p);

    // Age flashes
    this.flashes = this.flashes.filter(f => ++f.t < f.maxT);

    this._steps++;
    if (this._steps % 30 === 0) {
      this._rate = this._recentReactions / 30;
      this._recentReactions = 0;
    }
    if (this._steps % 20 === 0) {
      this._recordHistory();
      if (this.onUpdate) this.onUpdate(this.info());
    }
  }

  _tryReact(p, q, dx, dy, dist, toRemove, toAdd) {
    const isAB = (p.type === 'A' && q.type === 'B') || (p.type === 'B' && q.type === 'A');
    if (!isAB) return false;

    // Arrhenius factor: k ∝ exp(-Ea / T)
    if (Math.random() > Math.exp(-this.Ea / this.T) * 0.38) return false;

    const m1 = p.r * p.r, m2 = q.r * q.r, mt = m1 + m2;
    const cx  = (p.x * m1 + q.x * m2) / mt;
    const cy  = (p.y * m1 + q.y * m2) / mt;
    const pvx = (p.vx * m1 + q.vx * m2) / mt;
    const pvy = (p.vy * m1 + q.vy * m2) / mt;

    toRemove.add(p.id);
    toRemove.add(q.id);

    if (this.mode === 'chain') {
      // Chain: A + B <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> 2 C (two fast products — cascade reaction)
      const spd = Math.sqrt(pvx * pvx + pvy * pvy) * 1.35 + this._baseSpeed('C') * 0.7;
      const ang  = Math.atan2(pvy || 0.001, pvx || 0.001);
      for (let s = 0; s < 2; s++) {
        const sign = s === 0 ? 1 : -1;
        toAdd.push({
          x:  cx + Math.cos(ang) * sign * 5,
          y:  cy + Math.sin(ang) * sign * 5,
          vx: Math.cos(ang) * sign * spd,
          vy: Math.sin(ang) * sign * spd,
          r: 6, type: 'C', id: this._nextId++
        });
      }
      this.flashes.push({ x: cx, y: cy, t: 0, maxT: 28, color: '255,140,30' });
    } else {
      // Forward / reversible: A + B <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> 1 C
      const cSpd = Math.sqrt(pvx * pvx + pvy * pvy) * 0.62 + this._baseSpeed('C') * 0.28;
      const ang  = Math.atan2(pvy || 0.001, pvx || 0.001);
      toAdd.push({ x: cx, y: cy, vx: Math.cos(ang) * cSpd, vy: Math.sin(ang) * cSpd, r: 7, type: 'C', id: this._nextId++ });
      this.flashes.push({ x: cx, y: cy, t: 0, maxT: 22, color: '255,200,50' });
    }

    this._totalReactions++;
    this._recentReactions++;

    // LabFX: flash spark + throttled tick sound at collision point
    if (window.LabFX) {
      const now = performance.now();
      if (!this._fxLastTick || now - this._fxLastTick > 200) {
        this._fxLastTick = now;
        LabFX.sound.play('tick', { volume: 0.1 });
      }
      LabFX.particles.emit({ ctx: this.ctx, x: cx, y: cy, count: 3,
        color: '#FFD166', speed: 45, spread: 3.14, angle: 0,
        gravity: 0, life: 200, shape: 'spark', glow: true });
    }

    return true;
  }

  _recordHistory() {
    let nA = 0, nB = 0, nC = 0;
    for (const p of this.particles) {
      if (p.type === 'A') nA++;
      else if (p.type === 'B') nB++;
      else nC++;
    }
    this._history.push({ step: this._steps, nA, nB, nC });
    if (this._history.length > 260) this._history.shift();
  }

  /* ────────────────────────── Rendering ────────────────────────── */

  draw() {
    const { ctx, W, H } = this;
    if (!W || !H) return;

    // ── Background ──
    ctx.fillStyle = '#080818';
    ctx.fillRect(0, 0, W, H);

    // ── Subtle dot grid ──
    ctx.fillStyle = 'rgba(255,255,255,0.033)';
    for (let x = 35; x < W; x += 35)
      for (let y = 35; y < H; y += 35) {
        ctx.beginPath();
        ctx.arc(x, y, 1, 0, Math.PI * 2);
        ctx.fill();
      }

    // ── Reaction flashes ──
    for (const f of this.flashes) {
      const prog   = f.t / f.maxT;
      const radius = prog * 48 + 4;
      const alpha  = (1 - prog) * 0.55;
      const g = ctx.createRadialGradient(f.x, f.y, 0, f.x, f.y, radius);
      g.addColorStop(0,   `rgba(${f.color},${alpha * 1.6})`);
      g.addColorStop(0.4, `rgba(${f.color},${alpha * 0.5})`);
      g.addColorStop(1,   `rgba(${f.color},0)`);
      ctx.fillStyle = g;
      ctx.beginPath();
      ctx.arc(f.x, f.y, radius, 0, Math.PI * 2);
      ctx.fill();
    }

    // ── Particles ──
    for (const p of this.particles) this._drawParticle(ctx, p);

    // ── Overlays ──
    this._drawLegend(ctx);
    this._drawConcentrationGraph(ctx);
    this._drawEnergyDiagram(ctx);

    // ── Empty state ──
    if (this.particles.length === 0) {
      ctx.fillStyle = 'rgba(255,255,255,0.22)';
      ctx.font = '14px sans-serif';
      ctx.textAlign = 'center';
      ctx.fillText('Все молекулы прореагировали — нажмите Сброс', W / 2, H / 2);
    }

    if (window.LabFX) LabFX.particles.draw(ctx);
  }

  _drawParticle(ctx, p) {
    const col = this._color(p.type);
    const { x, y, r } = p;

    // Outer glow
    const glow = ctx.createRadialGradient(x, y, 0, x, y, r * 3);
    glow.addColorStop(0, col + '50');
    glow.addColorStop(1, col + '00');
    ctx.fillStyle = glow;
    ctx.beginPath();
    ctx.arc(x, y, r * 3, 0, Math.PI * 2);
    ctx.fill();

    // Body (radial gradient for depth)
    const body = ctx.createRadialGradient(x - r * 0.28, y - r * 0.28, r * 0.05, x, y, r);
    body.addColorStop(0, col + 'ff');
    body.addColorStop(0.65, col + 'cc');
    body.addColorStop(1, col + '88');
    ctx.fillStyle = body;
    ctx.beginPath();
    ctx.arc(x, y, r, 0, Math.PI * 2);
    ctx.fill();

    // Specular highlight
    ctx.fillStyle = 'rgba(255,255,255,0.42)';
    ctx.beginPath();
    ctx.arc(x - r * 0.27, y - r * 0.27, r * 0.3, 0, Math.PI * 2);
    ctx.fill();

    // Type label
    ctx.fillStyle = 'rgba(0,0,0,0.72)';
    ctx.font = `bold ${Math.round(r * 1.15)}px sans-serif`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(p.type, x, y + 0.5);
    ctx.textBaseline = 'alphabetic';
  }

  _drawConcentrationGraph(ctx) {
    if (this._history.length < 2) return;
    const { W, H } = this;
    const gW = 198, gH = 118;
    const gX = W - gW - 10, gY = H - gH - 10;

    // Panel
    ctx.fillStyle = 'rgba(5,5,20,0.88)';
    ctx.strokeStyle = 'rgba(255,255,255,0.08)';
    ctx.lineWidth = 1;
    this._rrect(ctx, gX, gY, gW, gH, 7);
    ctx.fill(); ctx.stroke();

    // Title
    ctx.fillStyle = 'rgba(255,255,255,0.42)';
    ctx.font = '9px sans-serif';
    ctx.textAlign = 'left';
    ctx.fillText('Концентрация молекул', gX + 7, gY + 12);

    const pad = { l: 8, r: 6, t: 18, b: 24 };
    const px = gX + pad.l, py = gY + pad.t;
    const pw = gW - pad.l - pad.r, ph = gH - pad.t - pad.b;
    const maxN = this.N * 2.3;
    const n = this._history.length;

    // Grid lines
    ctx.strokeStyle = 'rgba(255,255,255,0.05)';
    ctx.lineWidth = 0.5;
    for (let i = 0; i <= 4; i++) {
      const yl = py + ph * (1 - i / 4);
      ctx.beginPath(); ctx.moveTo(px, yl); ctx.lineTo(px + pw, yl); ctx.stroke();
    }

    // Data lines
    const lines = [
      { key: 'nA', color: '#06D6E0', label: 'A — реагент' },
      { key: 'nB', color: '#EF476F', label: 'B — реагент' },
      { key: 'nC', color: '#FFD166', label: 'C — продукт' },
    ];
    for (const { key, color } of lines) {
      ctx.beginPath();
      ctx.strokeStyle = color;
      ctx.lineWidth = 1.6;
      for (let i = 0; i < n; i++) {
        const lx = px + (i / Math.max(n - 1, 1)) * pw;
        const ly = py + ph - Math.min(this._history[i][key] / maxN, 1) * ph;
        i === 0 ? ctx.moveTo(lx, ly) : ctx.lineTo(lx, ly);
      }
      ctx.stroke();
    }

    // Legend + current values
    const last = this._history[this._history.length - 1];
    lines.forEach(({ color, label }, i) => {
      const lx = gX + 8 + i * 58;
      ctx.fillStyle = color;
      ctx.fillRect(lx, gY + gH - 16, 11, 2.5);
      ctx.fillStyle = 'rgba(255,255,255,0.5)';
      ctx.font = '8px sans-serif';
      ctx.textAlign = 'left';
      ctx.fillText(label.split(' ')[0], lx + 13, gY + gH - 12);
    });
    ctx.fillStyle = 'rgba(255,255,255,0.3)';
    ctx.font = '8px monospace';
    ctx.textAlign = 'right';
    ctx.fillText(`A:${last.nA}  B:${last.nB}  C:${last.nC}`, gX + gW - 6, gY + gH - 12);
  }

  _drawEnergyDiagram(ctx) {
    const { W } = this;
    const dW = 158, dH = 100;
    const dX = W - dW - 10, dY = 10;

    ctx.fillStyle = 'rgba(5,5,20,0.88)';
    ctx.strokeStyle = 'rgba(255,255,255,0.08)';
    ctx.lineWidth = 1;
    this._rrect(ctx, dX, dY, dW, dH, 7);
    ctx.fill(); ctx.stroke();

    ctx.fillStyle = 'rgba(255,255,255,0.42)';
    ctx.font = '9px sans-serif';
    ctx.textAlign = 'left';
    ctx.fillText('Профиль энергии', dX + 7, dY + 12);

    const pad = { l: 22, r: 10, t: 18, b: 20 };
    const ex = dX + pad.l, ey_bot = dY + dH - pad.b;
    const ew = dW - pad.l - pad.r, eh = dH - pad.t - pad.b;

    const rE = 0.15;
    const tE = 0.85;
    const pE = Math.max(0.04, rE + this._diagDeltaH());
    const toY = e => ey_bot - e * eh;

    // Smooth reaction path
    ctx.beginPath();
    ctx.strokeStyle = 'rgba(255,200,60,0.78)';
    ctx.lineWidth = 2;
    ctx.moveTo(ex, toY(rE));
    ctx.lineTo(ex + ew * 0.15, toY(rE));
    ctx.bezierCurveTo(
      ex + ew * 0.32, toY(rE),
      ex + ew * 0.40, toY(tE),
      ex + ew * 0.50, toY(tE)
    );
    ctx.bezierCurveTo(
      ex + ew * 0.60, toY(tE),
      ex + ew * 0.68, toY(pE),
      ex + ew * 0.85, toY(pE)
    );
    ctx.lineTo(ex + ew, toY(pE));
    ctx.stroke();

    // Horizontal dashes at levels
    ctx.setLineDash([2, 3]);
    ctx.strokeStyle = 'rgba(255,255,255,0.1)';
    ctx.lineWidth = 0.75;
    [rE, pE].forEach(e => {
      ctx.beginPath(); ctx.moveTo(ex, toY(e)); ctx.lineTo(ex + ew, toY(e)); ctx.stroke();
    });
    ctx.setLineDash([]);

    // Ea bracket (left side)
    ctx.strokeStyle = 'rgba(255,255,255,0.28)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(ex - 3, toY(rE)); ctx.lineTo(ex - 8, toY(rE));
    ctx.moveTo(ex - 3, toY(tE)); ctx.lineTo(ex - 8, toY(tE));
    ctx.moveTo(ex - 7, toY(rE)); ctx.lineTo(ex - 7, toY(tE));
    ctx.stroke();
    ctx.fillStyle = 'rgba(255,255,255,0.38)';
    ctx.font = '8px sans-serif';
    ctx.textAlign = 'right';
    ctx.fillText('Ea', ex - 9, toY((rE + tE) / 2) + 3);

    // Labels
    ctx.fillStyle = '#06D6E0cc';
    ctx.font = '8px sans-serif';
    ctx.textAlign = 'left';
    ctx.fillText('A+B', ex, toY(rE) - 4);
    ctx.fillStyle = '#FFD166cc';
    ctx.textAlign = 'right';
    ctx.fillText('C', ex + ew, toY(pE) - 4);

    // Mode label at bottom
    const modeTxt = { forward: '→  A + B → C', reversible: '⇌  A + B ⇌ C', chain: 'цепная реакция' }[this.mode] || '';
    ctx.fillStyle = 'rgba(255,255,255,0.22)';
    ctx.font = '8px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(modeTxt, dX + dW / 2, dY + dH - 6);
  }

  _diagDeltaH() {
    // Visual ΔH for energy diagram: exothermic by default
    return -(0.10 + this.Ea * 0.045);
  }

  _drawLegend(ctx) {
    const items = [
      { color: '#06D6E0', label: 'A — реагент' },
      { color: '#EF476F', label: 'B — реагент' },
      { color: '#FFD166', label: 'C — продукт' },
    ];
    const lX = 10, lY = 10, lW = 120, lH = 14 * items.length + 14;
    ctx.fillStyle = 'rgba(5,5,20,0.78)';
    ctx.strokeStyle = 'rgba(255,255,255,0.07)';
    ctx.lineWidth = 1;
    this._rrect(ctx, lX, lY, lW, lH, 6);
    ctx.fill(); ctx.stroke();

    items.forEach(({ color, label }, i) => {
      const iy = lY + 14 + i * 14;
      ctx.fillStyle = color;
      ctx.beginPath();
      ctx.arc(lX + 12, iy, 4.5, 0, Math.PI * 2);
      ctx.fill();
      ctx.fillStyle = 'rgba(255,255,255,0.52)';
      ctx.font = '9px sans-serif';
      ctx.textAlign = 'left';
      ctx.fillText(label, lX + 22, iy + 3.5);
    });
  }

  _rrect(ctx, x, y, w, h, r) {
    ctx.beginPath();
    ctx.moveTo(x + r, y);
    ctx.lineTo(x + w - r, y);
    ctx.quadraticCurveTo(x + w, y, x + w, y + r);
    ctx.lineTo(x + w, y + h - r);
    ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
    ctx.lineTo(x + r, y + h);
    ctx.quadraticCurveTo(x, y + h, x, y + h - r);
    ctx.lineTo(x, y + r);
    ctx.quadraticCurveTo(x, y, x + r, y);
    ctx.closePath();
  }
}

/* ─── lab UI init ─────────────────────────────────── */
  function _openChemistry(mode) {
    document.getElementById('sim-topbar-title').textContent = 'Химические реакции';
    _simShow('sim-chemistry');
    _simShow('ctrl-chemistry');
    if (mode) _chemMode = mode;
    requestAnimationFrame(() => requestAnimationFrame(() => {
      chemMode(_chemMode);
    }));
  }

  function chemMode(mode, btn) {
    _chemMode = mode;
    const MODES = ['kinetics', 'flask', 'redox', 'ionex'];
    const CANVASES = { kinetics: 'reactions-canvas', flask: 'flask-canvas', redox: 'redox-canvas', ionex: 'ionexchange-canvas' };

    // toggle mode buttons
    document.querySelectorAll('.chem-mode').forEach(b => b.classList.remove('active'));
    const mb = document.getElementById('chem-mode-' + mode);
    if (mb) mb.classList.add('active');

    // toggle panels
    MODES.forEach(m => {
      const p = document.getElementById('chem-panel-' + m);
      if (p) p.style.display = m === mode ? '' : 'none';
    });

    // toggle canvases
    Object.entries(CANVASES).forEach(([m, cid]) => {
      document.getElementById(cid).style.display = m === mode ? 'block' : 'none';
    });

    // toggle topbar tool groups
    const modeToCtrl = { kinetics:'kin', flask:'flask', redox:'redox', ionex:'ionex' };
    ['kin', 'flask', 'redox', 'ionex'].forEach(k => {
      const el = document.getElementById('ctrl-chem-' + k);
      if (el) el.style.display = k === modeToCtrl[mode] ? 'contents' : 'none';
    });

    // stop all sims
    if (reacSim)  reacSim.stop();
    if (flaskSim) flaskSim.stop();
    if (rdxSim)   rdxSim.stop();
    if (ioxSim)   ioxSim.stop();

    // start the active one
    if (mode === 'kinetics') {
      const c = document.getElementById('reactions-canvas');
      if (!reacSim) { reacSim = new ReactionSim(c); reacSim.onUpdate = _reacUpdateUI; }
      reacSim.fit(); reacSim.start();
      _reacUpdateUI(reacSim.info());
    } else if (mode === 'flask') {
      const c = document.getElementById('flask-canvas');
      if (!flaskSim) { flaskSim = new FlaskSim(c); flaskSim.onUpdate = _flaskUpdateUI; }
      flaskSim.fit(); flaskSim.start();
      _flaskUpdateUI(flaskSim.info());
    } else if (mode === 'redox') {
      const c = document.getElementById('redox-canvas');
      if (!rdxSim) { rdxSim = new RedoxSim(c); rdxSim.onUpdate = _redoxUpdateUI; }
      rdxSim.fit(); rdxSim.draw();
      _redoxUpdateUI(rdxSim.info());
    } else if (mode === 'ionex') {
      const c = document.getElementById('ionexchange-canvas');
      if (!ioxSim) { ioxSim = new IonExSim(c); ioxSim.onUpdate = _ionexUpdateUI; }
      ioxSim.fit(); ioxSim.draw();
      _ionexUpdateUI(ioxSim.info());
    }
  }

  function chemReset() {
    if (window.LabFX) LabFX.sound.play('whoosh', { volume: 0.3 });
    if (_chemMode === 'kinetics' && reacSim)  reacSim.reset();
    if (_chemMode === 'flask'    && flaskSim) flaskSim.reset();
    if (_chemMode === 'redox')   redoxReset();
    if (_chemMode === 'ionex')   ionexReset();
  }

  // _openReactions is now handled by _openChemistry + chemMode

  function reacNChange() {
    const v = +document.getElementById('sl-reacN').value;
    document.getElementById('reac-N-val').textContent = v;
    if (reacSim) reacSim.setN(v);
  }

  function reacTChange() {
    const raw = +document.getElementById('sl-reacT').value;
    const t = (raw / 10).toFixed(1);
    document.getElementById('reac-T-val').textContent = t;
    if (reacSim) reacSim.setT(+t);
  }

  function reacEaChange() {
    const raw = +document.getElementById('sl-reacEa').value;
    const ea = (raw / 10).toFixed(1);
    document.getElementById('reac-Ea-val').textContent = ea;
    if (reacSim) reacSim.setEa(+ea);
  }

  function reacMode(mode, el) {
    if (window.LabFX) LabFX.sound.play('click');
    if (reacSim) reacSim.setMode(mode);
    document.querySelectorAll('.reac-mode-btn').forEach(b => b.classList.remove('active'));
    if (el) el.classList.add('active');
  }

  function reacPreset(name) {
    if (!reacSim) return;
    reacSim.preset(name);
    // Sync sliders and mode buttons
    document.getElementById('sl-reacN').value  = reacSim.N;
    document.getElementById('reac-N-val').textContent = reacSim.N;
    document.getElementById('sl-reacT').value  = Math.round(reacSim.T * 10);
    document.getElementById('reac-T-val').textContent = reacSim.T.toFixed(1);
    document.getElementById('sl-reacEa').value = Math.round(reacSim.Ea * 10);
    document.getElementById('reac-Ea-val').textContent = reacSim.Ea.toFixed(1);
    document.querySelectorAll('.reac-mode-btn').forEach(b => b.classList.remove('active'));
    const mBtn = document.getElementById('rmode-' + reacSim.mode);
    if (mBtn) mBtn.classList.add('active');
    _reacUpdateUI(reacSim.info());
  }

  function reacTogglePause() {
    if (!reacSim) return;
    reacSim.toggleReaction();
    const btn = document.getElementById('reac-pause-btn');
    btn.innerHTML = reacSim.reactionOn ? '<svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg> Пауза' : '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg> Реакции';
  }

  function _reacUpdateUI(info) {
    if (!info) return;
    document.getElementById('chbar-l1').textContent = 'A молекул';
    document.getElementById('chbar-v1').textContent = info.nA;
    document.getElementById('chbar-l2').textContent = 'B молекул';
    document.getElementById('chbar-v2').textContent = info.nB;
    document.getElementById('chbar-l3').textContent = 'C продукт';
    document.getElementById('chbar-v3').textContent = info.nC;
    document.getElementById('chbar-l4').textContent = 'Реакций';
    document.getElementById('chbar-v4').textContent = info.reactions;
    document.getElementById('chbar-l5').textContent = 'Скорость';
    document.getElementById('chbar-v5').textContent = info.rate > 0
      ? (info.rate * 30).toFixed(1) + '/с' : '—';
  }

  // _openFlask is now handled by _openChemistry('flask')

  function flaskMetal(type, el) {
    if (flaskSim) { flaskSim.setMetal(type); flaskSim.reset(); }
    document.querySelectorAll('.flask-metal-btn').forEach(b => b.classList.remove('active'));
    if (el) el.classList.add('active');
  }

  function flaskAcid(type, el) {
    if (flaskSim) flaskSim.setAcid(type);
    document.querySelectorAll('.flask-acid-btn').forEach(b => b.classList.remove('active'));
    if (el) el.classList.add('active');
  }

  function flaskConcChange() {
    const v = +document.getElementById('sl-flask-conc').value;
    document.getElementById('flask-conc-val').textContent = v + '%';
    if (flaskSim) flaskSim.setConc(v / 100);
  }

  function flaskTempChange() {
    const v = +document.getElementById('sl-flask-temp').value;
    document.getElementById('flask-temp-val').textContent = v + '°C';
    if (flaskSim) flaskSim.setEnvTemp(v);
  }

  function flaskToggleFlame() {
    if (!flaskSim) return;
    flaskSim.toggleFlame();
    const active = flaskSim._flameOn;
    document.getElementById('flask-flame-btn').style.opacity     = active ? '1' : '0.5';
    document.getElementById('flask-flame-panel').style.opacity   = active ? '1' : '0.5';
    document.getElementById('flask-flame-panel').style.background = active ? 'rgba(239,71,111,0.22)' : '';
  }

  function flaskTogglePause() {
    if (!flaskSim) return;
    flaskSim.togglePause();
    document.getElementById('flask-pause-btn').innerHTML = flaskSim._paused ? '<svg class="ic" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>' : '<svg class="ic" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>';
  }

  function _flaskUpdateUI(info) {
    if (!info) return;
    document.getElementById('chbar-l1').textContent = 'Металл';
    document.getElementById('chbar-v1').textContent = info.metal;
    document.getElementById('chbar-l2').textContent = 'Масса';
    document.getElementById('chbar-v2').textContent = info.mass + ' г';
    document.getElementById('chbar-l3').textContent = 'T (°C)';
    document.getElementById('chbar-v3').textContent = info.temp + '°C';
    document.getElementById('chbar-l4').textContent = 'pH';
    document.getElementById('chbar-v4').textContent = info.pH;
    document.getElementById('chbar-l5').textContent = 'H₂ (%)';
    document.getElementById('chbar-v5').textContent = info.h2pct + '%';
  }

  // _openRedox is now handled by _openChemistry('redox')

  function redoxRxn(id, el) {
    document.querySelectorAll('.redox-rxn-btn').forEach(b => b.classList.remove('active'));
    if (el) el.classList.add('active');
    if (rdxSim) { rdxSim.setReaction(id); }
  }

  function redoxStart() {
    if (rdxSim) rdxSim.start();
  }

  function redoxReset() {
    if (window.LabFX) LabFX.sound.play('whoosh', { volume: 0.3 });
    if (rdxSim) rdxSim.reset();
  }

  function _redoxUpdateUI(info) {
    if (!info) return;
    const phaseMap = { idle: 'ожидание', mixing: 'смешивание', reacting: 'реакция', done: 'завершена' };
    document.getElementById('chbar-l1').textContent = 'Реакция';
    document.getElementById('chbar-v1').textContent = info.rxn || '—';
    document.getElementById('chbar-l2').textContent = 'Фаза';
    document.getElementById('chbar-v2').textContent = phaseMap[info.phase] || info.phase;
    document.getElementById('chbar-l3').textContent = 'Прогресс';
    document.getElementById('chbar-v3').textContent = info.phase === 'done' ? '100%' : info.prog + '%';
    document.getElementById('chbar-l4').textContent = 'Электронов';
    document.getElementById('chbar-v4').textContent = info.e + ' e⁻';
    document.getElementById('chbar-l5').textContent = 'Тип';
    document.getElementById('chbar-v5').innerHTML = info.phase === 'done' ? '<svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>' : '—';
  }

  // _openIonExchange is now handled by _openChemistry('ionex')

  function ionexRxn(id, el) {
    document.querySelectorAll('.ionex-rxn-btn').forEach(b => b.classList.remove('active'));
    if (el) el.classList.add('active');
    if (ioxSim) { ioxSim.setReaction(id); }
  }

  function ionexStart() {
    if (ioxSim) ioxSim.start();
  }

  function ionexReset() {
    if (window.LabFX) LabFX.sound.play('whoosh', { volume: 0.3 });
    if (ioxSim) ioxSim.reset();
  }

  function _ionexUpdateUI(info) {
    if (!info) return;
    const phaseMap = { idle: 'ожидание', mixing: 'смешивание', pairing: 'реакция', done: 'завершена' };
    const rxn = IonExSim.RXN[ioxSim.rxnId];
    document.getElementById('chbar-l1').textContent = 'Реакция';
    document.getElementById('chbar-v1').textContent = info.rxn || '—';
    document.getElementById('chbar-l2').textContent = 'Фаза';
    document.getElementById('chbar-v2').textContent = phaseMap[info.phase] || info.phase;
    document.getElementById('chbar-l3').textContent = 'Прогресс';
    document.getElementById('chbar-v3').textContent = info.phase === 'done' ? '100%' : info.prog + '%';
    document.getElementById('chbar-l4').textContent = 'Осадок';
    document.getElementById('chbar-v4').textContent = info.precip > 0 ? info.precip + ' ч.' : '—';
    document.getElementById('chbar-l5').textContent = 'Продукт';
    document.getElementById('chbar-v5').textContent = rxn ? (rxn.sign || '—') : '—';
  }

  /* ════════════════════════════════
     ЗАКОНЫ НЬЮТОНА
  ════════════════════════════════ */

  /* ══════════════════════════════
     DYNAMICS (unified Newton + Sandbox)
     ══════════════════════════════ */

