'use strict';

/**
 * EquilibriumSim — Chemical equilibrium simulation.
 * A + B ⇌ C + D with Arrhenius kinetics, Le Chatelier principle.
 * Left: particle animation with collisions & reactions.
 * Right (30%): live concentration graph over time.
 */
class EquilibriumSim {
  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, nD}]
    this._nextId   = 0;

    /* parameters */
    this.T    = 300;           // temperature K
    this.nA   = 20;            // initial A count
    this.nB   = 20;            // initial B count
    this.Ea_f = 50;            // forward activation energy
    this.Ea_r = 55;            // reverse activation energy

    /* runtime */
    this._steps  = 0;
    this._raf    = null;
    this._dpr    = 1;
    this.playing = false;
    this.onUpdate = null;

    new ResizeObserver(() => { this.fit(); this.draw(); }).observe(canvas.parentElement);
  }

  /* ═══════════════════════ public API ═══════════════════════ */

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

  getParams() { return { T: this.T, nA: this.nA, nB: this.nB, Ea_f: this.Ea_f, Ea_r: this.Ea_r }; }
  setParams({ T, nA, nB, Ea_f, Ea_r } = {}) {
    let needReset = false;
    if (T    !== undefined) this.T    = Math.max(200, Math.min(500, +T));
    if (Ea_f !== undefined) this.Ea_f = +Ea_f;
    if (Ea_r !== undefined) this.Ea_r = +Ea_r;
    if (nA   !== undefined) { this.nA = Math.max(10, Math.min(40, +nA)); needReset = true; }
    if (nB   !== undefined) { this.nB = Math.max(10, Math.min(40, +nB)); needReset = true; }
    if (needReset) this.reset();
    this.draw();
    this._emit();
  }

  preset(name) {
    const presets = {
      default:      { T: 300, nA: 20, nB: 20, Ea_f: 50, Ea_r: 55 },
      exothermic:   { T: 280, nA: 20, nB: 20, Ea_f: 35, Ea_r: 65 },
      endothermic:  { T: 350, nA: 20, nB: 20, Ea_f: 65, Ea_r: 35 },
      excess_A:     { T: 300, nA: 35, nB: 15, Ea_f: 50, Ea_r: 55 },
    };
    const p = presets[name] || presets.default;
    Object.assign(this, p);
    this.reset();
  }

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

    const simW = W * 0.7;
    this._spawnType('A', this.nA, simW);
    this._spawnType('B', this.nB, simW);
    this._recordHistory();
    this.draw();
    this._emit();
  }

  play() {
    if (this.playing) return;
    this.playing = true;
    this._tick();
  }

  pause() {
    this.playing = false;
    if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; }
  }

  start() { this.play(); }
  stop()  { this.pause(); }

  info() {
    let nA = 0, nB = 0, nC = 0, nD = 0;
    for (const p of this.particles) {
      if (p.type === 'A') nA++;
      else if (p.type === 'B') nB++;
      else if (p.type === 'C') nC++;
      else nD++;
    }
    const cA = nA || 0.001, cB = nB || 0.001;
    const cC = nC || 0.001, cD = nD || 0.001;
    const Q   = (cC * cD) / (cA * cB);
    const keq = Math.exp((this.Ea_f - this.Ea_r) / (this.T * 0.05));
    const direction = Q < keq * 0.95 ? '\u2192' : Q > keq * 1.05 ? '\u2190' : '\u21CC';
    return { keq: +keq.toFixed(3), Q: +Q.toFixed(3), direction, nA, nB, nC, nD };
  }

  /* ═══════════════════════ internals ═══════════════════════ */

  _emit() { if (this.onUpdate) this.onUpdate(this.info()); }

  _tick() {
    if (!this.playing) return;
    this._raf = requestAnimationFrame((ts) => {
      const dt = 0.016; // ~60fps fixed step for LabFX
      if (window.LabFX) LabFX.particles.update(dt);
      for (let i = 0; i < 3; i++) this._step();
      this.draw();
      this._tick();
    });
  }

  _color(type) {
    return { A: '#EF476F', B: '#9B5DE5', C: '#7BF5A4', D: '#FFD166' }[type] || '#aaa';
  }

  _radius() { return 5; }

  _spawnType(type, count, maxX) {
    const { H } = this;
    const r = this._radius();
    const margin = 10;
    let placed = 0, att = 0;
    while (placed < count && att < count * 60) {
      att++;
      const x = margin + r + Math.random() * (maxX - 2 * r - margin * 2);
      const y = margin + r + Math.random() * (H - 2 * r - margin * 2);
      let overlap = false;
      for (const p of this.particles) {
        if ((p.x - x) ** 2 + (p.y - y) ** 2 < (p.r + r + 1) ** 2) { overlap = true; break; }
      }
      if (overlap) continue;
      const a = Math.random() * Math.PI * 2;
      const spd = 1.5 + Math.random() * 1.5;
      this.particles.push({ x, y, vx: Math.cos(a) * spd, vy: Math.sin(a) * spd, r, type, id: this._nextId++ });
      placed++;
    }
  }

  _step() {
    const { W, H } = this;
    const simW = W * 0.7;
    const dt = 0.6;

    /* move + walls */
    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 > simW - p.r) { p.x = simW - 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); }
    }

    /* spatial grid */
    const cs = 18;
    const cols = Math.ceil(simW / cs) + 1;
    const grid = new Map();
    for (let i = 0; i < this.particles.length; i++) {
      const p = this.particles[i];
      const k = Math.floor(p.x / cs) + Math.floor(p.y / cs) * cols;
      if (!grid.has(k)) grid.set(k, []);
      grid.get(k).push(i);
    }

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

    /* collisions + reactions */
    for (let i = 0; i < this.particles.length; i++) {
      const p1 = this.particles[i];
      if (toRemove.has(p1.id)) continue;
      const cx = Math.floor(p1.x / cs), cy = Math.floor(p1.y / cs);
      for (let dcx = -1; dcx <= 1; dcx++) for (let dcy = -1; dcy <= 1; dcy++) {
        const cell = grid.get((cx + dcx) + (cy + dcy) * cols);
        if (!cell) continue;
        for (const j of cell) {
          if (j <= i) continue;
          const p2 = this.particles[j];
          if (toRemove.has(p2.id)) continue;
          const dx = p2.x - p1.x, dy = p2.y - p1.y;
          const dist2 = dx * dx + dy * dy;
          const minD  = p1.r + p2.r;
          if (dist2 >= minD * minD) continue;
          const dist = Math.sqrt(dist2);

          /* forward: 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 + D */
          const isAB = (p1.type === 'A' && p2.type === 'B') || (p1.type === 'B' && p2.type === 'A');
          if (isAB) {
            const kf = Math.exp(-this.Ea_f / (this.T * 0.08)) * 0.35;
            if (Math.random() < kf) {
              toRemove.add(p1.id); toRemove.add(p2.id);
              const mx = (p1.x + p2.x) / 2, my = (p1.y + p2.y) / 2;
              const a1 = Math.random() * Math.PI * 2;
              const spd = 1.2 + Math.random();
              toAdd.push({ x: mx + Math.cos(a1) * 4, y: my + Math.sin(a1) * 4, vx: Math.cos(a1) * spd, vy: Math.sin(a1) * spd, r: 5, type: 'C', id: this._nextId++ });
              toAdd.push({ x: mx - Math.cos(a1) * 4, y: my - Math.sin(a1) * 4, vx: -Math.cos(a1) * spd, vy: -Math.sin(a1) * spd, r: 5, type: 'D', id: this._nextId++ });
              this.flashes.push({ x: mx, y: my, t: 0, maxT: 18, color: '123,245,164' });
              continue;
            }
          }

          /* reverse: C + D <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 */
          const isCD = (p1.type === 'C' && p2.type === 'D') || (p1.type === 'D' && p2.type === 'C');
          if (isCD) {
            const kr = Math.exp(-this.Ea_r / (this.T * 0.08)) * 0.35;
            if (Math.random() < kr) {
              toRemove.add(p1.id); toRemove.add(p2.id);
              const mx = (p1.x + p2.x) / 2, my = (p1.y + p2.y) / 2;
              const a1 = Math.random() * Math.PI * 2;
              const spd = 1.2 + Math.random();
              toAdd.push({ x: mx + Math.cos(a1) * 4, y: my + Math.sin(a1) * 4, vx: Math.cos(a1) * spd, vy: Math.sin(a1) * spd, r: 5, type: 'A', id: this._nextId++ });
              toAdd.push({ x: mx - Math.cos(a1) * 4, y: my - Math.sin(a1) * 4, vx: -Math.cos(a1) * spd, vy: -Math.sin(a1) * spd, r: 5, type: 'B', id: this._nextId++ });
              this.flashes.push({ x: mx, y: my, t: 0, maxT: 18, color: '239,71,111' });
              continue;
            }
          }

          /* elastic bounce */
          if (dist > 0.001) {
            const nx = dx / dist, ny = dy / dist;
            const dvn = (p1.vx - p2.vx) * nx + (p1.vy - p2.vy) * ny;
            if (dvn > 0) {
              p1.vx -= dvn * nx; p1.vy -= dvn * ny;
              p2.vx += dvn * nx; p2.vy += dvn * ny;
            }
            const ov = (minD - dist) * 0.5;
            p1.x -= nx * ov; p1.y -= ny * ov;
            p2.x += nx * ov; p2.y += ny * ov;
          }
        }
      }
    }

    if (toRemove.size) {
      // LabFX: throttled tick sound + spark on each collision
      if (window.LabFX && toRemove.size > 0) {
        const now = performance.now();
        if (!this._lastFxTick || now - this._lastFxTick > 200) {
          this._lastFxTick = now;
          LabFX.sound.play('tick', { volume: 0.1 });
        }
        for (const id of toRemove) {
          const hit = this.particles.find(p => p.id === id);
          if (hit) {
            LabFX.particles.emit({ ctx: this.ctx, x: hit.x, y: hit.y, count: 2,
              color: '#FFD166', speed: 30, spread: 3.14, angle: 0,
              gravity: 0, life: 250, shape: 'spark', glow: true });
          }
        }
      }
      this.particles = this.particles.filter(p => !toRemove.has(p.id));
    }
    for (const p of toAdd) this.particles.push(p);
    this.flashes = this.flashes.filter(f => ++f.t < f.maxT);

    this._steps++;
    if (this._steps % 20 === 0) {
      this._recordHistory();
      this._emit();
    }
  }

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

  /* ═══════════════════════ rendering ═══════════════════════ */

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

    /* background */
    ctx.fillStyle = '#0D0D1A';
    ctx.fillRect(0, 0, W, H);

    /* dot grid */
    ctx.fillStyle = 'rgba(255,255,255,0.025)';
    for (let x = 30; x < simW; x += 30)
      for (let y = 30; y < H; y += 30) {
        ctx.beginPath(); ctx.arc(x, y, 0.8, 0, Math.PI * 2); ctx.fill();
      }

    /* divider */
    ctx.fillStyle = 'rgba(255,255,255,0.06)';
    ctx.fillRect(simW - 1, 0, 2, H);

    /* flashes */
    for (const f of this.flashes) {
      const prog   = f.t / f.maxT;
      const radius = prog * 38 + 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.5})`);
      g.addColorStop(0.4, `rgba(${f.color},${alpha * 0.4})`);
      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);

    /* right panel: concentration graph */
    this._drawGraph(ctx, simW, W, H);

    /* stats overlay */
    this._drawStats(ctx);

    /* equation label */
    ctx.fillStyle = 'rgba(255,255,255,0.28)';
    ctx.font = "bold 11px 'Manrope', system-ui, sans-serif";
    ctx.textAlign = 'center';
    ctx.fillText('A + B  \u21CC  C + D', simW / 2, H - 12);

    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.2);
    glow.addColorStop(0, col + '44');
    glow.addColorStop(1, col + '00');
    ctx.fillStyle = glow;
    ctx.beginPath(); ctx.arc(x, y, r * 3.2, 0, Math.PI * 2); ctx.fill();

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

    /* specular */
    ctx.fillStyle = 'rgba(255,255,255,0.38)';
    ctx.beginPath(); ctx.arc(x - r * 0.28, y - r * 0.28, r * 0.28, 0, Math.PI * 2); ctx.fill();

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

  _drawGraph(ctx, x0, W, H) {
    const gW = W - x0, pad = { l: 36, r: 10, t: 32, b: 28 };
    const px = x0 + pad.l, py = pad.t;
    const pw = gW - pad.l - pad.r;
    const ph = H - pad.t - pad.b;

    /* panel bg */
    ctx.fillStyle = 'rgba(5,5,20,0.85)';
    ctx.fillRect(x0, 0, gW, H);

    /* title */
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.font = "10px 'Manrope', system-ui, sans-serif";
    ctx.textAlign = 'left';
    ctx.fillText('\u041A\u043E\u043D\u0446\u0435\u043D\u0442\u0440\u0430\u0446\u0438\u044F', x0 + 10, 16);

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

    /* y-axis labels */
    ctx.fillStyle = 'rgba(255,255,255,0.25)';
    ctx.font = "8px 'Manrope', system-ui, sans-serif";
    ctx.textAlign = 'right';
    const maxN = Math.max(this.nA, this.nB) * 1.2 + 2;
    for (let i = 0; i <= 4; i++) {
      const v = Math.round(maxN * (4 - i) / 4);
      ctx.fillText(v, px - 4, py + ph * (i / 4) + 3);
    }

    if (this._history.length < 2) return;
    const n = this._history.length;

    const lines = [
      { key: 'nA', color: '#EF476F', label: 'A' },
      { key: 'nB', color: '#9B5DE5', label: 'B' },
      { key: 'nC', color: '#7BF5A4', label: 'C' },
      { key: 'nD', color: '#FFD166', label: 'D' },
    ];

    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 */
    lines.forEach(({ color, label }, i) => {
      const lx = x0 + 10 + i * 38;
      const ly = H - 14;
      ctx.fillStyle = color;
      ctx.fillRect(lx, ly, 10, 2.5);
      ctx.fillStyle = 'rgba(255,255,255,0.5)';
      ctx.font = "9px 'Manrope', system-ui, sans-serif";
      ctx.textAlign = 'left';
      ctx.fillText(label, lx + 13, ly + 3);
    });

    /* current values */
    const last = this._history[n - 1];
    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} D:${last.nD}`, x0 + gW - 8, H - 14);
  }

  _drawStats(ctx) {
    const info = this.info();
    const px = 10, py = 10, pw = 160, ph = 82;

    ctx.fillStyle = 'rgba(5,5,20,0.82)';
    ctx.beginPath(); ctx.roundRect(px, py, pw, ph, 7); ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.07)'; ctx.lineWidth = 1; ctx.stroke();

    ctx.textAlign = 'left'; ctx.textBaseline = 'top';
    ctx.font = "10px 'Manrope', system-ui, sans-serif";
    const lh = 16;

    ctx.fillStyle = '#7BF5A4';
    ctx.fillText(`K\u2091\u2071 = ${info.keq}`, px + 10, py + 8);

    ctx.fillStyle = '#FFD166';
    ctx.fillText(`Q = ${info.Q}`, px + 10, py + 8 + lh);

    ctx.fillStyle = '#06D6E0';
    ctx.fillText(`\u041D\u0430\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435: ${info.direction}`, px + 10, py + 8 + lh * 2);

    ctx.fillStyle = 'rgba(255,255,255,0.45)';
    ctx.fillText(`T = ${this.T} K`, px + 10, py + 8 + lh * 3);
  }

  /* ═══════════════════════ utility ═══════════════════════ */

  _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();
  }
}

if (typeof module !== 'undefined') module.exports = EquilibriumSim;

/* ─── lab UI init ─────────────────────────────────── */
  function _openEquilibrium() {
    document.getElementById('sim-topbar-title').textContent = 'Химическое равновесие';
    _simShow('sim-equilibrium');
    _registerSimState('equilibrium', () => eqSim?.getParams(), st => eqSim?.setParams(st));
    if (_embedMode) _startStateEmit('equilibrium');
    requestAnimationFrame(() => requestAnimationFrame(() => {
      if (!eqSim) {
        eqSim = new EquilibriumSim(document.getElementById('equilibrium-canvas'));
        eqSim.onUpdate = _eqUpdateUI;
      }
      eqSim.fit();
      eqSim.reset();
      eqSim.play();
    }));
  }

  function eqParam(name, val) {
    const v = parseFloat(val);
    const ids = { T: 'eq-T-val', Ea_f: 'eq-Eaf-val', Ea_r: 'eq-Ear-val' };
    const el = document.getElementById(ids[name]);
    if (el) el.textContent = v;
    if (name === 'T' && window.LabFX) LabFX.sound.play('whoosh', { pitch: v / 300, volume: 0.3 });
    if (eqSim) eqSim.setParams({ [name]: v });
  }

  function eqPreset(name) {
    if (window.LabFX) LabFX.sound.play('pour', { volume: 0.3 });
    if (eqSim) { eqSim.preset(name); eqSim.play(); }
    const defs = { default: [300,50,55], exothermic: [280,35,65], endothermic: [350,65,35], excess_A: [300,50,55] };
    const d = defs[name] || defs.default;
    document.getElementById('sl-eq-T').value = d[0]; document.getElementById('eq-T-val').textContent = d[0];
    document.getElementById('sl-eq-Eaf').value = d[1]; document.getElementById('eq-Eaf-val').textContent = d[1];
    document.getElementById('sl-eq-Ear').value = d[2]; document.getElementById('eq-Ear-val').textContent = d[2];
  }

  function _eqUpdateUI(info) {
    const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
    v('eqbar-v1', info.keq);
    v('eqbar-v2', info.Q);
    v('eqbar-v3', info.direction);
    v('eqbar-v4', info.nA + '|' + info.nB + '|' + info.nC + '|' + info.nD);
  }

  /* ── thin lens ── */

