'use strict';
/* ════════════════════════════════════════════════════════════════
   PhotosynthesisSim — Фотосинтез и клеточное дыхание
   Световые реакции · цикл Кальвина · митохондриальное дыхание
   Молекулярная анимация · частицы · статистика
   ════════════════════════════════════════════════════════════════ */

class PhotosynthesisSim {

  static C = {
    bg:          '#0a0e14',
    // хлоропласт
    chlorBg:     'rgba(34,211,153,0.07)',
    chlorStroke: 'rgba(34,211,153,0.5)',
    thylBg:      'rgba(34,211,153,0.18)',
    thylStroke:  'rgba(34,211,153,0.6)',
    stroma:      'rgba(34,211,153,0.04)',
    // митохондрия
    mitoBg:      'rgba(239,71,111,0.08)',
    mitoStroke:  'rgba(239,71,111,0.5)',
    cristaeBg:   'rgba(239,71,111,0.18)',
    cristaeStroke:'rgba(239,71,111,0.55)',
    matrix:      'rgba(239,71,111,0.04)',
    // молекулы
    photon:      '#FFD166',
    water:       '#06D6E0',
    co2:         '#EF476F',
    o2:          '#4CC9F0',
    atp:         '#9B5DE5',
    nadph:       '#7BF5A4',
    g3p:         '#22d399',
    glucose:     '#FFD166',
    pyruvate:    '#FF6B35',
    electron:    '#4CC9F0',
    // text
    label:       'rgba(255,255,255,0.35)',
    labelBright: 'rgba(255,255,255,0.8)',
  };

  constructor(canvas) {
    this.canvas = canvas;
    this.ctx    = canvas.getContext('2d');

    this.mode   = 'photo';    // 'photo' | 'resp'
    this._light = 70;         // 0..100
    this._co2   = 50;         // 0..100

    this._particles = [];
    this._time      = 0;

    this._stats = { atp: 0, atpRate: 0, o2: 0, co2Out: 0, efficiency: 0 };
    this._atpAccum   = 0;
    this._atpSmoothR = 0;

    // spawn timers
    this._photonTimer   = 0;
    this._waterTimer    = 0;
    this._co2Timer      = 0;
    this._glucoseTimer  = 0;
    this._atpTimer      = 0;
    this._pyrTimer      = 0;
    this._krebsAngle    = 0;
    this._etcOffset     = 0;

    // LabFX throttle timers
    this._fxPhotonThrottle  = 0;
    this._fxAtpSound        = 0;
    this._fxGlucoseSound    = 0;

    // layout (computed in fit)
    this._layout = {};

    this._raf  = null;
    this._last = 0;
    this.W = 0; this.H = 0;

    this.onUpdate = null;

    this.fit();
  }

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

  fit() {
    const dpr = window.devicePixelRatio || 1;
    const W   = this.canvas.offsetWidth  || 700;
    const H   = this.canvas.offsetHeight || 440;
    this.canvas.width  = Math.round(W * dpr);
    this.canvas.height = Math.round(H * dpr);
    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    this.W = W; this.H = H;
    this._calcLayout();
    if (!this._raf) this._draw();
  }

  start() {
    if (this._raf) return;
    this._last = performance.now();
    const loop = t => { this._raf = requestAnimationFrame(loop); this._tick(t); };
    this._raf = requestAnimationFrame(loop);
  }

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

  reset() {
    this._particles = [];
    this._time      = 0;
    this._stats     = { atp: 0, atpRate: 0, o2: 0, co2Out: 0, efficiency: 0 };
    this._atpAccum  = 0;
    this._atpSmoothR = 0;
    if (!this._raf) this._draw();
    this._emitUpdate();
  }

  setMode(mode) {
    this.mode = mode;
    if (window.LabFX) LabFX.sound.play('whoosh', { volume: 0.3 });
    this.reset();
  }

  setLightIntensity(v) { this._light = v; }
  setCO2(v)            { this._co2   = v; }

  /* ── Layout ───────────────────────────────────────────────── */

  _calcLayout() {
    const { W, H } = this;
    const cx = W / 2, cy = H / 2;
    const ow = Math.min(W * 0.82, 560), oh = Math.min(H * 0.72, 300);

    if (this.mode === 'photo' || this.mode !== 'resp') {
      // chloroplast outer
      this._layout = {
        cx, cy,
        outerRx: ow / 2, outerRy: oh / 2,
        // thylakoid band (horizontal, middle third)
        thylY:  cy - oh * 0.06,
        thylH:  oh * 0.28,
        thylX1: cx - ow * 0.4,
        thylX2: cx + ow * 0.4,
        // stroma top / bottom
        stromaTopY: cy - oh / 2,
        stromaBotY: cy + oh / 2,
        // label positions
        thylLabelY: cy + oh * 0.08,
        stromaLabelY: cy - oh * 0.34,
      };
    } else {
      // mitochondria
      this._layout = {
        cx, cy,
        outerRx: ow / 2, outerRy: oh / 2,
        // inner membrane (cristae zone: inner 60% of organelle)
        innerRx: ow * 0.3,
        innerRy: oh * 0.3,
        // zones
        matrixCx: cx + ow * 0.12,
        cytoCx:   cx - ow * 0.32,
        etcY:     cy,
      };
    }
  }

  /* ── Tick ─────────────────────────────────────────────────── */

  _tick(t) {
    const dt = Math.min(t - this._last, 80);
    this._last = t;
    this._time += dt;

    if (this.mode === 'photo') {
      this._updatePhoto(dt);
    } else {
      this._updateResp(dt);
    }

    if (window.LabFX) LabFX.particles.update(dt);
    this._updateParticles(dt);
    this._draw();
    this._emitUpdate();
  }

  /* ── Photosynthesis update ────────────────────────────────── */

  _updatePhoto(dt) {
    const L  = this._light / 100;
    const CO = this._co2   / 100;
    const rate = L * 0.8 + 0.2;   // min rate even at low light

    // фотоны (rain from top)
    this._photonTimer += dt;
    const photonInterval = 300 / (L * 3 + 0.5);
    while (this._photonTimer > photonInterval) {
      this._photonTimer -= photonInterval;
      this._spawnPhoton();
      // throttled photon absorption sound (~5/sec max)
      this._fxPhotonThrottle += photonInterval;
      if (this._fxPhotonThrottle >= 200 && window.LabFX) {
        LabFX.sound.play('tick', { pitch: 1.8, volume: 0.1 });
        this._fxPhotonThrottle = 0;
      }
    }

    // H2O splitting (thylakoid)
    this._waterTimer += dt;
    if (this._waterTimer > 600 / (rate + 0.2)) {
      this._waterTimer = 0;
      if (L > 0.1) this._spawnWaterSplit();
    }

    // CO2 into stroma
    this._co2Timer += dt;
    if (this._co2Timer > 500 / (CO * 2 + 0.3)) {
      this._co2Timer = 0;
      if (CO > 0.05) this._spawnCO2();
    }

    // ATP from thylakoid → stroma
    this._atpTimer += dt;
    if (this._atpTimer > 400 / (rate + 0.1)) {
      this._atpTimer = 0;
      if (L > 0.05) {
        this._spawnATP();
        // subtle chime on ATP formation (throttled)
        this._fxAtpSound += 400 / (rate + 0.1);
        if (this._fxAtpSound >= 1200 && window.LabFX) {
          LabFX.sound.play('chime', { pitch: 1.2, volume: 0.15 });
          this._fxAtpSound = 0;
        }
      }
    }

    // G3P output
    this._glucoseTimer += dt;
    if (this._glucoseTimer > 800 / (rate * CO + 0.1)) {
      this._glucoseTimer = 0;
      if (L > 0.1 && CO > 0.05) {
        this._spawnG3P();
        // Calvin cycle complete — glucose sparkle + chime (throttled)
        this._fxGlucoseSound = (this._fxGlucoseSound || 0) + 1;
        if (this._fxGlucoseSound >= 3 && window.LabFX) {
          this._fxGlucoseSound = 0;
          LabFX.sound.play('chime', { pitch: 0.8, volume: 0.3 });
          const L2 = this._layout;
          if (L2.cx) {
            LabFX.particles.emit({ ctx: this.ctx, x: L2.cx, y: L2.cy - (L2.thylH || 0) * 0.8,
              count: 8, color: '#FFD166', speed: 28, spread: Math.PI * 2, angle: 0,
              gravity: -8, life: 700, fade: true, glow: true, shape: 'spark', size: 3, sizeFade: true });
          }
        }
      }
    }

    // Calvin cycle rotation
    this._krebsAngle += dt * 0.0004 * rate;

    // stats
    const atpR = L * CO * 18;
    this._atpSmoothR += (atpR - this._atpSmoothR) * 0.05;
    this._atpAccum   += atpR * dt / 1000;
    this._stats.atpRate    = this._atpSmoothR;
    this._stats.atp        = this._atpAccum;
    this._stats.o2        += L * 0.4 * dt / 1000;
    this._stats.co2Out     = CO;
    this._stats.efficiency = Math.round(L * CO * 100 * 0.38);
  }

  /* ── Respiration update ───────────────────────────────────── */

  _updateResp(dt) {
    const rate = 0.8;

    // glucose <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> pyruvate (glycolysis)
    this._glucoseTimer += dt;
    if (this._glucoseTimer > 900) {
      this._glucoseTimer = 0;
      this._spawnGlucose();
    }

    // pyruvate <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> krebs cycle
    this._pyrTimer += dt;
    if (this._pyrTimer > 600) {
      this._pyrTimer = 0;
      this._spawnPyruvate();
    }

    // ATP bursts (ETC)
    this._atpTimer += dt;
    if (this._atpTimer > 350) {
      this._atpTimer = 0;
      this._spawnATPResp();
    }

    // CO2 from krebs
    this._co2Timer += dt;
    if (this._co2Timer > 500) {
      this._co2Timer = 0;
      this._spawnCO2Resp();
    }

    // electron flow along ETC
    this._etcOffset = (this._etcOffset + dt * 0.0008) % 1;
    this._krebsAngle += dt * 0.0005;

    // stats
    const atpR = 38 * 0.5;
    this._atpSmoothR += (atpR - this._atpSmoothR) * 0.04;
    this._atpAccum   += atpR * dt / 1000;
    this._stats.atpRate    = this._atpSmoothR;
    this._stats.atp        = this._atpAccum;
    this._stats.o2        += 0.3 * dt / 1000;
    this._stats.co2Out    += 0.5 * dt / 1000;
    this._stats.efficiency = 38;
  }

  /* ── Particle spawners ────────────────────────────────────── */

  _spawnPhoton() {
    const L = this._layout;
    const x = L.thylX1 + Math.random() * (L.thylX2 - L.thylX1);
    this._particles.push({
      type: 'photon', x, y: this.H * 0.02,
      vx: (Math.random() - 0.5) * 15,
      vy: 60 + Math.random() * 30,
      life: 1, maxLife: 1,
      targetY: L.thylY - L.thylH / 2,
    });
  }

  _spawnWaterSplit() {
    const L = this._layout;
    const x = L.cx - L.outerRx * 0.35 + Math.random() * L.outerRx * 0.15;
    const y = L.thylY;
    // O2 bubbles rise
    for (let i = 0; i < 2; i++) {
      this._particles.push({
        type: 'o2', x: x + i * 12, y,
        vx: (Math.random() - 0.5) * 20,
        vy: -(40 + Math.random() * 30),
        life: 1, maxLife: 1,
      });
    }
  }

  _spawnCO2() {
    const L = this._layout;
    const x = L.cx + L.outerRx * 0.3;
    const y = L.stromaTopY + Math.random() * (L.cy - L.stromaTopY);
    this._particles.push({
      type: 'co2', x, y,
      vx: -(30 + Math.random() * 20),
      vy: (Math.random() - 0.5) * 15,
      life: 1, maxLife: 1,
    });
  }

  _spawnATP() {
    const L = this._layout;
    const x = L.thylX1 + Math.random() * (L.thylX2 - L.thylX1);
    const y = L.thylY - L.thylH * 0.1;
    this._particles.push({
      type: 'atp', x, y,
      vx: (Math.random() - 0.5) * 25,
      vy: -(35 + Math.random() * 25),
      life: 1, maxLife: 1,
    });
  }

  _spawnG3P() {
    const L = this._layout;
    const angle = Math.random() * Math.PI * 2;
    const r = 30 + Math.random() * 20;
    this._particles.push({
      type: 'g3p',
      x: L.cx + Math.cos(angle) * r,
      y: L.cy - L.thylH * 0.8 + Math.sin(angle) * r * 0.5,
      vx: (Math.random() - 0.5) * 20,
      vy: -(20 + Math.random() * 15),
      life: 1, maxLife: 1,
    });
  }

  _spawnGlucose() {
    const L = this._layout;
    this._particles.push({
      type: 'glucose',
      x: L.cx - L.outerRx * 0.6,
      y: L.cy + (Math.random() - 0.5) * 40,
      vx: 35, vy: (Math.random() - 0.5) * 10,
      life: 1, maxLife: 1,
    });
  }

  _spawnPyruvate() {
    const L = this._layout;
    this._particles.push({
      type: 'pyruvate',
      x: L.cx - 20,
      y: L.cy + (Math.random() - 0.5) * 30,
      vx: 20, vy: (Math.random() - 0.5) * 15,
      life: 1, maxLife: 1,
    });
  }

  _spawnATPResp() {
    const L = this._layout;
    const angle = this._krebsAngle + Math.random() * 0.5;
    const r = L.innerRx * 0.7;
    this._particles.push({
      type: 'atp',
      x: L.cx + Math.cos(angle) * r,
      y: L.cy + Math.sin(angle) * r * 0.75,
      vx: (Math.random() - 0.5) * 30,
      vy: -(25 + Math.random() * 20),
      life: 1, maxLife: 1,
    });
  }

  _spawnCO2Resp() {
    const L = this._layout;
    const angle = this._krebsAngle + Math.PI * (0.5 + Math.random() * 0.5);
    const r = L.innerRx * 0.5;
    this._particles.push({
      type: 'co2',
      x: L.cx + Math.cos(angle) * r,
      y: L.cy + Math.sin(angle) * r * 0.75,
      vx: (Math.random() - 0.5) * 20,
      vy: -(30 + Math.random() * 20),
      life: 1, maxLife: 1,
    });
  }

  /* ── Particle update ──────────────────────────────────────── */

  _updateParticles(dt) {
    const s = dt / 1000;
    for (const p of this._particles) {
      if (p.targetY !== undefined && p.y > p.targetY) {
        p.y += p.vy * s;
        p.x += p.vx * s;
      } else {
        p.x += p.vx * s;
        p.y += p.vy * s;
        p.life -= s * (0.5 + Math.random() * 0.3);
      }
      if (p.type === 'photon' && p.y >= (p.targetY || 0)) {
        p.targetY = undefined;
        p.vy = -15;
        p.vx = (Math.random() - 0.5) * 30;
        p.life -= 0.4;
      }
    }
    // cap at 120 particles
    this._particles = this._particles.filter(p => p.life > 0).slice(-120);
  }

  /* ── Draw ─────────────────────────────────────────────────── */

  _draw() {
    const { ctx, W, H } = this;
    ctx.clearRect(0, 0, W, H);
    ctx.fillStyle = PhotosynthesisSim.C.bg;
    ctx.fillRect(0, 0, W, H);

    if (this.mode === 'photo') {
      this._drawChloroplast();
    } else {
      this._drawMitochondria();
    }

    this._drawParticles();
    this._drawEquation();
    if (window.LabFX) LabFX.particles.draw(this.ctx);
  }

  /* ── Chloroplast ──────────────────────────────────────────── */

  _drawChloroplast() {
    const { ctx } = this;
    const C = PhotosynthesisSim.C;
    const L = this._layout;
    if (!L.outerRx) return;

    // outer envelope
    ctx.beginPath();
    ctx.ellipse(L.cx, L.cy, L.outerRx, L.outerRy, 0, 0, Math.PI * 2);
    ctx.fillStyle = C.chlorBg;
    ctx.fill();
    ctx.strokeStyle = C.chlorStroke;
    ctx.lineWidth = 2.5;
    ctx.stroke();

    // stroma label
    ctx.save();
    ctx.font = '11px Manrope,sans-serif';
    ctx.fillStyle = 'rgba(34,211,153,0.45)';
    ctx.textAlign = 'center';
    ctx.fillText('Строма (цикл Кальвина)', L.cx, L.stromaTopY + 22);
    ctx.restore();

    // thylakoid membrane band
    const tY  = L.thylY, tH = L.thylH;
    const tX1 = L.thylX1, tX2 = L.thylX2;
    const tW  = tX2 - tX1;

    ctx.beginPath();
    _psRRect(ctx, tX1, tY - tH / 2, tW, tH, tH / 2);
    ctx.fillStyle = C.thylBg;
    ctx.fill();
    ctx.strokeStyle = C.thylStroke;
    ctx.lineWidth = 2;
    ctx.stroke();

    // thylakoid label
    ctx.save();
    ctx.font = '11px Manrope,sans-serif';
    ctx.fillStyle = 'rgba(34,211,153,0.6)';
    ctx.textAlign = 'center';
    ctx.fillText('Тилакоид (световые реакции)', L.cx, L.thylY + tH / 2 + 16);
    ctx.restore();

    // Calvin cycle rotating wheel in stroma
    this._drawCalvinCycle();

    // light arrows
    this._drawLightArrows();
  }

  _drawCalvinCycle() {
    const { ctx } = this;
    const C = PhotosynthesisSim.C;
    const L = this._layout;
    const cx = L.cx, cy = L.cy - L.thylH * 0.85;
    const r  = Math.min(L.outerRy * 0.28, 42);
    const a  = this._krebsAngle;

    ctx.save();
    ctx.globalAlpha = 0.6;

    // circle arrow (rotating)
    ctx.beginPath();
    ctx.arc(cx, cy, r, a, a + Math.PI * 1.7);
    ctx.strokeStyle = C.g3p;
    ctx.lineWidth = 2.5;
    ctx.stroke();

    // arrowhead
    const ex = cx + Math.cos(a + Math.PI * 1.7) * r;
    const ey = cy + Math.sin(a + Math.PI * 1.7) * r;
    const da = 0.4;
    ctx.beginPath();
    ctx.moveTo(ex, ey);
    ctx.lineTo(ex - Math.cos(a + Math.PI * 1.7 - da) * 8,
               ey - Math.sin(a + Math.PI * 1.7 - da) * 8);
    ctx.moveTo(ex, ey);
    ctx.lineTo(ex - Math.cos(a + Math.PI * 1.7 + da) * 8,
               ey - Math.sin(a + Math.PI * 1.7 + da) * 8);
    ctx.strokeStyle = C.g3p;
    ctx.lineWidth = 2;
    ctx.stroke();

    // center label
    ctx.globalAlpha = 0.55;
    ctx.font = 'bold 10px Manrope,sans-serif';
    ctx.fillStyle = C.g3p;
    ctx.textAlign = 'center';
    ctx.fillText('цикл', cx, cy - 2);
    ctx.fillText('Кальвина', cx, cy + 11);
    ctx.restore();
  }

  _drawLightArrows() {
    const { ctx } = this;
    const C = PhotosynthesisSim.C;
    const L = this._layout;
    const tX1 = L.thylX1, tX2 = L.thylX2;
    const topY = L.stromaTopY + 8;
    const botY = L.thylY - L.thylH / 2;
    const L_norm = this._light / 100;

    ctx.save();
    ctx.globalAlpha = 0.25 + L_norm * 0.55;
    const n = 5;
    for (let i = 0; i < n; i++) {
      const x = tX1 + (i + 0.5) / n * (tX2 - tX1);
      ctx.beginPath();
      ctx.moveTo(x, topY);
      ctx.lineTo(x, botY - 4);
      ctx.strokeStyle = C.photon;
      ctx.lineWidth = 1.5;
      ctx.setLineDash([5, 4]);
      ctx.stroke();
      ctx.setLineDash([]);
      // arrowhead
      ctx.beginPath();
      ctx.moveTo(x, botY - 4);
      ctx.lineTo(x - 5, botY - 14);
      ctx.moveTo(x, botY - 4);
      ctx.lineTo(x + 5, botY - 14);
      ctx.strokeStyle = C.photon;
      ctx.lineWidth = 1.5;
      ctx.stroke();
      // sun dot
      ctx.beginPath();
      ctx.arc(x, topY - 5, 4, 0, Math.PI * 2);
      ctx.fillStyle = C.photon;
      ctx.fill();
    }
    ctx.restore();
  }

  /* ── Mitochondria ─────────────────────────────────────────── */

  _drawMitochondria() {
    const { ctx } = this;
    const C = PhotosynthesisSim.C;
    const L = this._layout;
    if (!L.outerRx) return;

    // outer membrane
    ctx.beginPath();
    ctx.ellipse(L.cx, L.cy, L.outerRx, L.outerRy, 0, 0, Math.PI * 2);
    ctx.fillStyle = C.mitoBg;
    ctx.fill();
    ctx.strokeStyle = C.mitoStroke;
    ctx.lineWidth = 2.5;
    ctx.stroke();

    // inner membrane / cristae (zigzag folds)
    this._drawCristae();

    // zone labels
    ctx.save();
    ctx.font = '11px Manrope,sans-serif';
    ctx.textAlign = 'center';
    ctx.fillStyle = 'rgba(239,71,111,0.5)';
    ctx.fillText('Матрикс (цикл Кребса)', L.cx + L.innerRx * 0.1, L.cy + 12);
    ctx.fillStyle = 'rgba(239,71,111,0.35)';
    ctx.fillText('Цитоплазма (гликолиз)', L.cx - L.outerRx * 0.62, L.cy);
    ctx.restore();

    // Krebs cycle arrow
    this._drawKrebsWheel();

    // ETC along inner membrane
    this._drawETC();
  }

  _drawCristae() {
    const { ctx } = this;
    const C = PhotosynthesisSim.C;
    const L = this._layout;
    const iRx = L.innerRx || 110, iRy = L.innerRy || 80;

    ctx.beginPath();
    ctx.ellipse(L.cx, L.cy, iRx, iRy, 0, 0, Math.PI * 2);
    ctx.fillStyle = C.cristaeBg;
    ctx.fill();
    ctx.strokeStyle = C.cristaeStroke;
    ctx.lineWidth = 1.8;
    ctx.stroke();

    // cristae folds (vertical zigzag lines inside)
    ctx.save();
    ctx.globalAlpha = 0.45;
    ctx.strokeStyle = C.cristaeStroke;
    ctx.lineWidth = 1.5;
    for (let i = -2; i <= 2; i++) {
      const x = L.cx + i * iRx * 0.32;
      const h = Math.sqrt(Math.max(0, 1 - (i * 0.32) ** 2)) * iRy * 0.7;
      ctx.beginPath();
      ctx.moveTo(x, L.cy - h);
      ctx.bezierCurveTo(x - 12, L.cy - h * 0.3, x + 12, L.cy + h * 0.3, x, L.cy + h);
      ctx.stroke();
    }
    ctx.restore();
  }

  _drawKrebsWheel() {
    const { ctx } = this;
    const C = PhotosynthesisSim.C;
    const L = this._layout;
    const cx = L.cx, cy = L.cy;
    const r  = (L.innerRx || 110) * 0.42;
    const a  = this._krebsAngle;

    ctx.save();
    ctx.globalAlpha = 0.55;
    ctx.beginPath();
    ctx.arc(cx, cy, r, a, a + Math.PI * 1.65);
    ctx.strokeStyle = C.pyruvate;
    ctx.lineWidth = 2.5;
    ctx.stroke();
    // arrowhead
    const ex = cx + Math.cos(a + Math.PI * 1.65) * r;
    const ey = cy + Math.sin(a + Math.PI * 1.65) * r;
    const da = 0.45;
    ctx.beginPath();
    ctx.moveTo(ex, ey);
    ctx.lineTo(ex - Math.cos(a + Math.PI * 1.65 - da) * 8,
               ey - Math.sin(a + Math.PI * 1.65 - da) * 8);
    ctx.moveTo(ex, ey);
    ctx.lineTo(ex - Math.cos(a + Math.PI * 1.65 + da) * 8,
               ey - Math.sin(a + Math.PI * 1.65 + da) * 8);
    ctx.strokeStyle = C.pyruvate;
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.globalAlpha = 0.45;
    ctx.font = 'bold 10px Manrope,sans-serif';
    ctx.fillStyle = C.pyruvate;
    ctx.textAlign = 'center';
    ctx.fillText('цикл', cx, cy - 3);
    ctx.fillText('Кребса', cx, cy + 11);
    ctx.restore();
  }

  _drawETC() {
    const { ctx } = this;
    const C = PhotosynthesisSim.C;
    const L = this._layout;
    const iRx = L.innerRx || 110, iRy = L.innerRy || 80;
    const n = 8;

    ctx.save();
    ctx.globalAlpha = 0.7;
    for (let i = 0; i < n; i++) {
      const frac = ((i / n) + this._etcOffset) % 1;
      const a = frac * Math.PI * 2 - Math.PI / 2;
      const x = L.cx + Math.cos(a) * iRx;
      const y = L.cy + Math.sin(a) * iRy;
      const size = 4 + 2 * Math.sin(frac * Math.PI * 4);
      ctx.beginPath();
      ctx.arc(x, y, size, 0, Math.PI * 2);
      ctx.fillStyle = C.electron;
      ctx.shadowColor = C.electron;
      ctx.shadowBlur  = 6;
      ctx.fill();
      ctx.shadowBlur = 0;
    }
    ctx.restore();
  }

  /* ── Particle rendering ───────────────────────────────────── */

  _drawParticles() {
    const { ctx } = this;
    const C = PhotosynthesisSim.C;

    const colorMap = {
      photon:   C.photon,
      o2:       C.o2,
      co2:      C.co2,
      atp:      C.atp,
      nadph:    C.nadph,
      g3p:      C.g3p,
      glucose:  C.glucose,
      pyruvate: C.pyruvate,
      electron: C.electron,
    };
    const labelMap = {
      photon:   '*',
      o2:       'O₂',
      co2:      'CO₂',
      atp:      'ATP',
      nadph:    'NADPH',
      g3p:      'G3P',
      glucose:  'Глк',
      pyruvate: 'Пир',
      electron: 'e⁻',
    };

    for (const p of this._particles) {
      const alpha = Math.min(1, p.life * 2) * 0.9;
      if (alpha <= 0) continue;
      ctx.save();
      ctx.globalAlpha = alpha;
      const col = colorMap[p.type] || '#fff';
      const lbl = labelMap[p.type] || '';

      // glow
      ctx.beginPath();
      ctx.arc(p.x, p.y, 9, 0, Math.PI * 2);
      ctx.fillStyle = col + '28';
      ctx.fill();

      // circle
      ctx.beginPath();
      ctx.arc(p.x, p.y, 5, 0, Math.PI * 2);
      ctx.fillStyle = col;
      ctx.fill();

      // label
      ctx.font = 'bold 8px Manrope,sans-serif';
      ctx.fillStyle = '#fff';
      ctx.textAlign = 'center';
      ctx.fillText(lbl, p.x, p.y + 18);
      ctx.restore();
    }
  }

  /* ── Equation footer ──────────────────────────────────────── */

  _drawEquation() {
    const { ctx, W, H } = this;
    const eq = this.mode === 'photo'
      ? '6CO₂ + 6H₂O + свет  →  C₆H₁₂O₆ + 6O₂'
      : 'C₆H₁₂O₆ + 6O₂  →  6CO₂ + 6H₂O + 38 ATP';

    ctx.save();
    ctx.font = '12px Manrope,sans-serif';
    const tw = ctx.measureText(eq).width;
    const px = W / 2 - tw / 2 - 12, py = H - 28;
    ctx.fillStyle = 'rgba(255,255,255,0.06)';
    _psRRect(ctx, px, py - 2, tw + 24, 20, 6);
    ctx.fill();
    ctx.fillStyle = 'rgba(255,255,255,0.45)';
    ctx.textAlign = 'center';
    ctx.fillText(eq, W / 2, py + 13);
    ctx.restore();
  }

  /* ── Stats emit ───────────────────────────────────────────── */

  _emitUpdate() {
    if (!this.onUpdate) return;
    this.onUpdate({
      mode:       this.mode,
      atpRate:    this._stats.atpRate.toFixed(1),
      o2:         Math.floor(this._stats.o2),
      co2:        Math.floor(this._stats.co2Out),
      efficiency: this._stats.efficiency.toFixed ? this._stats.efficiency.toFixed(0) : this._stats.efficiency,
      light:      this._light,
      co2Level:   this._co2,
    });
  }
}

/* helper */
function _psRRect(ctx, x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
}

/* ─── lab UI init ─────────────────────────────────── */
  function _openPhotosynthesis(mode) {
    document.getElementById('sim-topbar-title').textContent = 'Фотосинтез и дыхание';
    _simShow('sim-photosynthesis');
    _simShow('ctrl-photosynthesis');
    requestAnimationFrame(() => requestAnimationFrame(() => {
      const canvas = document.getElementById('photosyn-canvas');
      if (!photosynSim) {
        photosynSim = new PhotosynthesisSim(canvas);
        photosynSim.onUpdate = _psUpdateUI;
      }
      photosynSim.fit();
      photosynSim.setMode(mode || 'photo');
      photosynSim.start();
    }));
  }

  function psSetMode(mode, btn) {
    document.querySelectorAll('.ps-mode-btn').forEach(b => b.classList.remove('active'));
    if (btn) btn.classList.add('active');
    if (photosynSim) photosynSim.setMode(mode);
  }

  function psLightChange() {
    const v = +document.getElementById('sl-ps-light').value;
    document.getElementById('ps-light-val').textContent = v + '%';
    if (photosynSim) photosynSim.setLightIntensity(v);
  }

  function psCO2Change() {
    const v = +document.getElementById('sl-ps-co2').value;
    document.getElementById('ps-co2-val').textContent = v + '%';
    if (photosynSim) photosynSim.setCO2(v);
  }

  function psReset() {
    if (photosynSim) photosynSim.reset();
  }

  function _psUpdateUI(info) {
    const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
    v('psbar-v1', info.atpRate || '0');
    v('psbar-v2', info.o2 || '0');
    v('psbar-v3', info.co2 || '0');
    v('psbar-v4', info.efficiency ? info.efficiency + '%' : '—');
    v('psbar-v5', info.mode === 'photo' ? 'Фотосинтез' : 'Дыхание');
  }

  /* ── Angry Birds ── */
