'use strict';

/**
 * DiffusionSim v2 — Diffusion simulation (two gases mixing).
 * v2: entropy timeline on history chart, pore mode (gap in partition), density heatmap.
 */
class DiffusionSim {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx    = canvas.getContext('2d');
    this.W = 0; this.H = 0;
    this.particles   = [];
    this.N           = 60;
    this.T           = 1.0;
    this.partitionOn = true;
    this._history    = [];  // {step, fracA_left, entropy}
    this._steps      = 0;
    this._raf        = null;
    this.onUpdate    = null;
    this._dpr        = 1;

    // v2
    this._poreMode  = false;  // partition has a gap in the center
    this._poreH     = 40;     // gap height in pixels
    this._heatmap   = null;   // cached density heatmap
    this._hmTick    = 0;

    // LabFX
    this._fxLastT        = 0;
    this._fxEquilDone    = false;  // equilibrium chime played once
  }

  // ── public API ──────────────────────────────────────────────────────────────
  fit() {
    const dpr = window.devicePixelRatio || 1;
    this._dpr = dpr;
    const w = this.canvas.offsetWidth, 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;
    this.partitionOn = true;
    this._poreMode   = false;
    this._steps      = 0;
    this._history    = [{ step: 0, fracA_left: 1.0, entropy: 0 }];
    this._heatmap    = null;

    const particles = [];
    const r = 5;

    let attA = 0;
    while (particles.filter(p => p.type === 'A').length < this.N && attA < this.N * 30) {
      attA++;
      const x = r + Math.random() * (W / 2 - 2 * r);
      const y = r + Math.random() * (H - 2 * r);
      const a = Math.random() * Math.PI * 2, s = this.T * 3.5;
      particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, r, type: 'A' });
    }

    let attB = 0;
    while (particles.filter(p => p.type === 'B').length < this.N && attB < this.N * 30) {
      attB++;
      const x = W / 2 + r + Math.random() * (W / 2 - 2 * r);
      const y = r + Math.random() * (H - 2 * r);
      const a = Math.random() * Math.PI * 2, s = this.T * 3.5;
      particles.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, r, type: 'B' });
    }

    this.particles = particles;
  }

  togglePartition() {
    if (this._poreMode) {
      // If pore is on, full toggle removes pore first
      this._poreMode  = false;
      this.partitionOn = true;
    } else {
      this.partitionOn = !this.partitionOn;
    }
    if (window.LabFX) LabFX.sound.play('whoosh', { pitch: 0.7, volume: 0.3 });
    this._fxEquilDone = false;  // allow equilibrium chime again after partition change
  }

  togglePore() {
    if (!this.partitionOn && !this._poreMode) {
      // Partition is fully off — re-enable with pore
      this.partitionOn = true;
      this._poreMode   = true;
    } else if (this.partitionOn && !this._poreMode) {
      this._poreMode   = true;   // add pore to full partition
    } else if (this._poreMode) {
      this._poreMode   = false;  // remove pore, keep partition
    }
  }

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

  setT(t) {
    const f = Math.sqrt(t / this.T);
    for (const p of this.particles) { p.vx *= f; p.vy *= f; }
    this.T = t;
  }

  start() { if (!this._raf) this._raf = requestAnimationFrame(this._loop.bind(this)); }
  stop()  { if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; } }

  // ── simulation ──────────────────────────────────────────────────────────────
  _loop(now) {
    const dt = this._fxLastT ? Math.min(now - this._fxLastT, 80) : 16;
    this._fxLastT = now;
    this._step(); this._step();
    if (window.LabFX) LabFX.particles.update(dt);
    this.draw();
    this._raf = requestAnimationFrame(this._loop.bind(this));
  }

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

    for (const p of particles) {
      p.x += p.vx; p.y += p.vy;

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

      // Partition logic
      if (this.partitionOn) {
        const mid = W / 2, hw = 3;
        const inPore = this._poreMode
          && p.y > H / 2 - this._poreH / 2
          && p.y < H / 2 + this._poreH / 2;

        if (!inPore) {
          if (p.vx > 0 && p.x + p.r > mid - hw && p.x < mid) {
            p.x = mid - hw - p.r; p.vx = -Math.abs(p.vx);
          } else if (p.vx < 0 && p.x - p.r < mid + hw && p.x > mid) {
            p.x = mid + hw + p.r; p.vx = Math.abs(p.vx);
          }
        }
      }
    }

    // Spatial grid collisions
    const cs = 14, cols = Math.ceil(W / cs) + 1;
    const grid = new Map();
    for (let i = 0; i < particles.length; i++) {
      const p = 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);
    }

    for (let i = 0; i < particles.length; i++) {
      const p1 = particles[i];
      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 = particles[j];
          const dx = p2.x - p1.x, dy = p2.y - p1.y;
          const d = Math.hypot(dx, dy), md = p1.r + p2.r;
          if (d < md && d > 0.001) {
            const nx = dx / d, ny = dy / d;
            const dvn = (p1.vx - p2.vx) * nx + (p1.vy - p2.vy) * ny;
            if (dvn < 0) continue;
            p1.vx -= dvn * nx; p1.vy -= dvn * ny;
            p2.vx += dvn * nx; p2.vy += dvn * ny;
            const ov = (md - d) / 2;
            p1.x -= nx * ov; p1.y -= ny * ov;
            p2.x += nx * ov; p2.y += ny * ov;
          }
        }
      }
    }

    // History (with entropy)
    if (this._steps % 60 === 0) {
      const left = particles.filter(p => p.x < W / 2);
      const fracA_left = left.length > 0
        ? left.filter(p => p.type === 'A').length / left.length
        : 0;
      const f = fracA_left;
      const entropy = -(f * Math.log(f + 1e-9) + (1 - f) * Math.log(1 - f + 1e-9));
      this._history.push({ step: this._steps, fracA_left, entropy });
      if (this._history.length > 200) this._history.shift();
    }

    // Heatmap update (every 30 steps)
    if (this._steps % 30 === 0) this._updateHeatmap();

    this._steps++;
    if (this._steps % 30 === 0 && this.onUpdate) this.onUpdate(this.info());

    // Equilibrium detection: mixed >= 45% and partition is off
    if (!this.partitionOn && !this._fxEquilDone && this._steps % 60 === 0 && window.LabFX) {
      const info = this.info();
      if (+info.mixed >= 45) {
        this._fxEquilDone = true;
        LabFX.sound.play('chime', { pitch: 1.0, volume: 0.3 });
      }
    }
  }

  _updateHeatmap() {
    const { W, H, particles } = this;
    const cols = 20, rows = 14;
    const cw = W / cols, ch = H / rows;
    const grid = [];
    for (let r = 0; r < rows; r++) {
      grid[r] = [];
      for (let c = 0; c < cols; c++) grid[r][c] = { A: 0, B: 0 };
    }
    for (const p of particles) {
      const c = Math.min(cols - 1, Math.floor(p.x / cw));
      const r = Math.min(rows - 1, Math.floor(p.y / ch));
      grid[r][c][p.type]++;
    }
    const maxCount = Math.max(...grid.flat().map(c => c.A + c.B), 1);
    this._heatmap = { grid, cols, rows, cw, ch, maxCount };
  }

  info() {
    const { particles, W, N } = this;
    const leftA  = particles.filter(p => p.x < W / 2 && p.type === 'A').length;
    const leftB  = particles.filter(p => p.x < W / 2 && p.type === 'B').length;
    const rightA = particles.filter(p => p.x >= W / 2 && p.type === 'A').length;
    const rightB = particles.filter(p => p.x >= W / 2 && p.type === 'B').length;
    const mixed  = (leftB + rightA) / (2 * N);
    const fracAL = leftA / ((leftA + leftB) || 1);
    const entropy = -(fracAL * Math.log(fracAL + 1e-9) + (1 - fracAL) * Math.log(1 - fracAL + 1e-9));
    return {
      leftA, leftB, rightA, rightB,
      mixed:        (mixed * 100).toFixed(0),
      entropy:      entropy.toFixed(3),
      partitionOn:  this.partitionOn,
      poreMode:     this._poreMode,
      steps:        this._steps,
    };
  }

  // ── drawing ─────────────────────────────────────────────────────────────────
  draw() {
    const { ctx, W, H } = this;
    const TAU = Math.PI * 2;

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

    // Background tints
    ctx.fillStyle = 'rgba(6,214,224,0.04)';  ctx.fillRect(0,     0, W / 2, H);
    ctx.fillStyle = 'rgba(241,91,181,0.04)'; ctx.fillRect(W / 2, 0, W / 2, H);

    // Density heatmap (subtle)
    this._drawHeatmap(ctx);

    // Partition
    if (this.partitionOn) this._drawPartition(ctx, W, H);

    // Particles
    ctx.save();
    for (const p of this.particles) {
      const color = p.type === 'A' ? '#06D6E0' : '#F15BB5';
      ctx.shadowColor = color; ctx.shadowBlur = 6;
      ctx.fillStyle   = color;
      ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, TAU); ctx.fill();
    }
    ctx.restore();

    // Concentration bar (right side)
    this._drawConcBar(ctx, W, H);

    // History chart with entropy (bottom)
    this._drawHistoryChart(ctx, W, H);

    // Stats overlay (top-left)
    this._drawStats(ctx);
    if (window.LabFX) LabFX.particles.draw(ctx);
  }

  _drawHeatmap(ctx) {
    const hm = this._heatmap;
    if (!hm) return;
    for (let r = 0; r < hm.rows; r++) for (let c = 0; c < hm.cols; c++) {
      const cell = hm.grid[r][c];
      const total = cell.A + cell.B;
      if (total === 0) continue;
      const frac = total / hm.maxCount;
      // Color based on A vs B ratio
      const fracA = cell.A / total;
      // Mix cyan and pink by composition
      const rr = Math.round(6   + (241 - 6)   * (1 - fracA));
      const rg = Math.round(214 + (91  - 214) * (1 - fracA));
      const rb = Math.round(224 + (181 - 224) * (1 - fracA));
      ctx.fillStyle = `rgba(${rr},${rg},${rb},${frac * 0.08})`;
      ctx.fillRect(c * hm.cw, r * hm.ch, hm.cw, hm.ch);
    }
  }

  _drawPartition(ctx, W, H) {
    const mid = W / 2, pw = 6;
    const poreOn = this._poreMode;
    const poreY1 = H / 2 - this._poreH / 2;
    const poreY2 = H / 2 + this._poreH / 2;

    ctx.save();
    ctx.shadowBlur = 10; ctx.shadowColor = 'rgba(255,255,255,0.5)';

    const grad = ctx.createLinearGradient(mid - pw / 2, 0, mid + pw / 2, 0);
    grad.addColorStop(0, 'rgba(255,255,255,0.15)');
    grad.addColorStop(1, 'rgba(255,255,255,0.05)');
    ctx.fillStyle = grad;

    if (!poreOn) {
      ctx.fillRect(mid - pw / 2, 0, pw, H);
    } else {
      // Two segments (above and below pore)
      ctx.fillRect(mid - pw / 2, 0, pw, poreY1);
      ctx.fillRect(mid - pw / 2, poreY2, pw, H - poreY2);

      // Pore opening highlight
      ctx.shadowBlur = 0;
      ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.lineWidth = 1;
      ctx.setLineDash([3, 3]);
      ctx.beginPath(); ctx.moveTo(mid - pw / 2, poreY1); ctx.lineTo(mid + pw / 2, poreY1); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(mid - pw / 2, poreY2); ctx.lineTo(mid + pw / 2, poreY2); ctx.stroke();
      ctx.setLineDash([]);

      // Pore gap arrows (showing flow direction)
      ctx.fillStyle = 'rgba(255,255,255,0.4)';
      ctx.font = "bold 10px monospace"; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText('⇌', mid, H / 2);
    }

    if (!poreOn) {
      // Door handle
      const hx = mid - 10, hy = H / 2 - 14, hw = 20, hh = 28;
      ctx.shadowBlur = 0;
      ctx.fillStyle = 'rgba(255,255,255,0.12)';
      ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 1;
      ctx.beginPath(); ctx.roundRect(hx, hy, hw, hh, 4); ctx.fill(); ctx.stroke();
      ctx.fillStyle = 'rgba(255,255,255,0.6)';
      ctx.font = "bold 10px monospace"; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText('||', mid, H / 2);
    }

    ctx.restore();
  }

  _drawConcBar(ctx, W, H) {
    const barX = W - 20, barHalf = H / 2;
    const { particles } = this;
    const lA = particles.filter(p => p.x < W / 2 && p.type === 'A').length;
    const lT = particles.filter(p => p.x < W / 2).length || 1;
    const rA = particles.filter(p => p.x >= W / 2 && p.type === 'A').length;
    const rT = particles.filter(p => p.x >= W / 2).length || 1;
    const fAL = lA / lT, fAR = rA / rT;

    ctx.fillStyle = '#06D6E0'; ctx.fillRect(barX, 0,           20, barHalf * fAL);
    ctx.fillStyle = '#F15BB5'; ctx.fillRect(barX, barHalf * fAL, 20, barHalf * (1 - fAL));
    ctx.fillStyle = '#06D6E0'; ctx.fillRect(barX, barHalf,         20, barHalf * fAR);
    ctx.fillStyle = '#F15BB5'; ctx.fillRect(barX, barHalf + barHalf * fAR, 20, barHalf * (1 - fAR));

    ctx.fillStyle = 'rgba(255,255,255,0.2)'; ctx.fillRect(barX, barHalf - 1, 20, 2);
    ctx.strokeStyle = 'rgba(255,255,255,0.15)'; ctx.lineWidth = 1;
    ctx.strokeRect(barX, 0, 20, H);

    ctx.save();
    ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.font = "9px 'Manrope', sans-serif";
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.translate(barX + 10, H / 2); ctx.rotate(-Math.PI / 2);
    ctx.fillText('Концентрация', 0, 0);
    ctx.restore();
  }

  _drawHistoryChart(ctx, W, H) {
    const graphH = 100, graphY = H - graphH, graphW = W - 24;

    ctx.save();
    ctx.fillStyle = 'rgba(0,0,10,0.76)';
    ctx.beginPath(); ctx.roundRect(0, graphY, graphW, graphH, 8); ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.lineWidth = 1; ctx.stroke();

    ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.font = "10px 'Manrope', sans-serif";
    ctx.textAlign = 'left'; ctx.textBaseline = 'top';
    ctx.fillText('Доля A в левой половине', 10, graphY + 6);

    // Y-axis labels
    ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.font = "9px 'Manrope', sans-serif";
    ctx.fillText('1.0', 4, graphY + 18);
    ctx.fillText('0.0', 4, graphY + graphH - 10);

    const refY = graphY + graphH * 0.5 - 2;
    ctx.setLineDash([4, 4]); ctx.strokeStyle = 'rgba(255,255,255,0.2)'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.moveTo(24, refY); ctx.lineTo(graphW - 10, refY); ctx.stroke();
    ctx.setLineDash([]);
    ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.font = "9px 'Manrope', sans-serif";
    ctx.textAlign = 'left'; ctx.fillText('равновесие', 28, refY - 10);

    const hist = this._history;
    if (hist.length > 1) {
      const plotX0 = 28, plotW = graphW - 38;
      const plotY0 = graphY + 18, plotH2 = graphH - 28;

      // Concentration line (cyan)
      ctx.strokeStyle = '#06D6E0'; ctx.lineWidth = 1.5;
      ctx.beginPath();
      for (let i = 0; i < hist.length; i++) {
        const hx = plotX0 + (i / (hist.length - 1)) * plotW;
        const hy = plotY0 + plotH2 * (1 - hist[i].fracA_left);
        if (i === 0) ctx.moveTo(hx, hy); else ctx.lineTo(hx, hy);
      }
      ctx.stroke();

      // Entropy line (orange, dashed, scaled to 0..ln(2) ≈ 0.693)
      const maxEnt = Math.log(2);
      ctx.strokeStyle = '#FFB347'; ctx.lineWidth = 1.2;
      ctx.setLineDash([4, 3]);
      ctx.beginPath();
      for (let i = 0; i < hist.length; i++) {
        const hx = plotX0 + (i / (hist.length - 1)) * plotW;
        const hy = plotY0 + plotH2 * (1 - hist[i].entropy / maxEnt);
        if (i === 0) ctx.moveTo(hx, hy); else ctx.lineTo(hx, hy);
      }
      ctx.stroke();
      ctx.setLineDash([]);

      // Legend
      ctx.fillStyle = '#06D6E0'; ctx.beginPath(); ctx.arc(plotX0 + plotW - 50, graphY + 8, 3, 0, Math.PI * 2); ctx.fill();
      ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.font = "8px sans-serif"; ctx.textBaseline = 'middle';
      ctx.textAlign = 'left'; ctx.fillText('X(A)', plotX0 + plotW - 44, graphY + 8);

      ctx.fillStyle = '#FFB347'; ctx.beginPath(); ctx.arc(plotX0 + plotW - 22, graphY + 8, 3, 0, Math.PI * 2); ctx.fill();
      ctx.fillText('S', plotX0 + plotW - 16, graphY + 8);

      // Current value
      const last = hist[hist.length - 1];
      const endX = plotX0 + plotW;
      const endY = plotY0 + plotH2 * (1 - last.fracA_left);
      ctx.fillStyle = '#06D6E0'; ctx.font = "bold 10px 'Manrope', sans-serif";
      ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
      ctx.fillText((last.fracA_left * 100).toFixed(0) + '%', endX - 2, endY);
    }

    ctx.restore();
  }

  _drawStats(ctx) {
    const info = this.info();
    const pad = 10, panelW = 180, panelH = 90, px = 14, py = 14;

    ctx.save();
    ctx.fillStyle = 'rgba(0,0,10,0.72)';
    ctx.beginPath(); ctx.roundRect(px, py, panelW, panelH, 8); ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.07)'; ctx.lineWidth = 1; ctx.stroke();

    const lineH = 18;
    ctx.textAlign = 'left'; ctx.textBaseline = 'top'; ctx.font = "11px 'Manrope', sans-serif";

    ctx.fillStyle = '#06D6E0';
    ctx.fillText(`Лево:  A=${info.leftA}  B=${info.leftB}`, px + pad, py + pad);

    ctx.fillStyle = '#F15BB5';
    ctx.fillText(`Право: A=${info.rightA}  B=${info.rightB}`, px + pad, py + pad + lineH);

    ctx.fillStyle = 'rgba(255,255,255,0.8)';
    ctx.fillText(`Смешивание: ${info.mixed}%`, px + pad, py + pad + lineH * 2);

    const stateLabel = !info.partitionOn ? 'Снята' : info.poreMode ? 'С порой' : 'Вкл';
    const stateColor = !info.partitionOn ? '#F15BB5' : info.poreMode ? '#FFB347' : '#06D6E0';
    ctx.fillStyle = stateColor;
    ctx.fillText(`Раздел: ${stateLabel}`, px + pad, py + pad + lineH * 3);

    ctx.restore();
  }
}

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