'use strict';

/**
 * BrownianSim v2 — Brownian Motion simulation.
 * v2: age-gradient trail, MSD history chart, hover tooltip on big particle,
 *     resetOrigin() method.
 */
class BrownianSim {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx    = canvas.getContext('2d');
    this.W = 0; this.H = 0;
    this.big   = { x: 0, y: 0, vx: 0, vy: 0, r: 22 };
    this.small = [];
    this.N     = 120;
    this.T     = 1.0;
    this.trail = [];
    this._origin   = { x: 0, y: 0 };
    this._steps    = 0;
    this._raf      = null;
    this.onUpdate  = null;
    this._dpr      = 1;
    this._fxLastT  = 0;

    // v2
    this._msdHistory = [];  // [{step, msd}]
    this._hover      = false;

    canvas.addEventListener('mousemove',  e => this._onMouseMove(e));
    canvas.addEventListener('mouseleave', () => { this._hover = false; });
  }

  _cp(e) {
    const r = this.canvas.getBoundingClientRect();
    return {
      x: (e.clientX - r.left) * (this.W / r.width),
      y: (e.clientY - r.top)  * (this.H / r.height),
    };
  }

  _onMouseMove(e) {
    const { x, y } = this._cp(e);
    this._hover = Math.hypot(x - this.big.x, y - this.big.y) < this.big.r + 22;
  }

  // ── 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.big    = { x: W / 2, y: H / 2, vx: 0, vy: 0, r: 22 };
    this._origin = { x: W / 2, y: H / 2 };
    this.trail   = [{ x: W / 2, y: H / 2 }];
    this._steps  = 0;
    this._msdHistory = [];

    const small = [];
    let att = 0;
    while (small.length < this.N && att < this.N * 20) {
      att++;
      const r  = 4;
      const x  = r + Math.random() * (W - 2 * r);
      const y  = r + Math.random() * (H - 2 * r);
      if (Math.hypot(x - W / 2, y - H / 2) < this.big.r + r + 8) continue;
      const a = Math.random() * Math.PI * 2, s = this.T * 4.5;
      small.push({ x, y, vx: Math.cos(a) * s, vy: Math.sin(a) * s, r });
    }
    this.small = small;
  }

  resetOrigin() {
    this._origin = { x: this.big.x, y: this.big.y };
    this._msdHistory = [];
  }

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

  setT(t) {
    const f = Math.sqrt(t / this.T);
    for (const s of this.small) { s.vx *= f; s.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(); this._step();
    if (window.LabFX) LabFX.particles.update(dt);
    this.draw();
    this._raf = requestAnimationFrame(this._loop.bind(this));
  }

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

    big.x += big.vx; big.y += big.vy;
    if (big.x < big.r) { big.x = big.r; big.vx = Math.abs(big.vx); }
    if (big.x > W - big.r) { big.x = W - big.r; big.vx = -Math.abs(big.vx); }
    if (big.y < big.r) { big.y = big.r; big.vy = Math.abs(big.vy); }
    if (big.y > H - big.r) { big.y = H - big.r; big.vy = -Math.abs(big.vy); }

    for (const s of small) {
      s.x += s.vx; s.y += s.vy;
      if (s.x < s.r) { s.x = s.r; s.vx = Math.abs(s.vx); }
      if (s.x > W - s.r) { s.x = W - s.r; s.vx = -Math.abs(s.vx); }
      if (s.y < s.r) { s.y = s.r; s.vy = Math.abs(s.vy); }
      if (s.y > H - s.r) { s.y = H - s.r; s.vy = -Math.abs(s.vy); }
    }

    // big vs small
    const m1 = big.r * big.r;
    for (const s of small) {
      const dx = s.x - big.x, dy = s.y - big.y;
      const dist = Math.hypot(dx, dy), md = big.r + s.r;
      if (dist < md && dist > 0.001) {
        const nx = dx / dist, ny = dy / dist;
        const dvn = (big.vx - s.vx) * nx + (big.vy - s.vy) * ny;
        if (dvn > 0) continue;
        const m2 = s.r * s.r, imp = (2 * dvn) / (m1 + m2);
        big.vx -= imp * m2 * nx; big.vy -= imp * m2 * ny;
        s.vx   += imp * m1 * nx; s.vy   += imp * m1 * ny;
        const ov = md - dist, f1 = m2 / (m1 + m2), f2 = m1 / (m1 + m2);
        big.x -= nx * ov * f1; big.y -= ny * ov * f1;
        s.x   += nx * ov * f2; s.y   += ny * ov * f2;
      }
    }

    // small vs small — spatial grid
    const cs = 10, cols = Math.ceil(W / cs) + 1;
    const grid = new Map();
    for (let i = 0; i < small.length; i++) {
      const s = small[i];
      const k = Math.floor(s.x / cs) + Math.floor(s.y / cs) * cols;
      if (!grid.has(k)) grid.set(k, []);
      grid.get(k).push(i);
    }
    const checked = new Set();
    for (let i = 0; i < small.length; i++) {
      const s1 = small[i];
      const cx = Math.floor(s1.x / cs), cy = Math.floor(s1.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 pk = i * 10000 + j;
          if (checked.has(pk)) continue;
          checked.add(pk);
          const s2 = small[j];
          const dx = s2.x - s1.x, dy = s2.y - s1.y;
          const d = Math.hypot(dx, dy), md = s1.r + s2.r;
          if (d < md && d > 0.001) {
            const nx = dx / d, ny = dy / d;
            const dvn = (s1.vx - s2.vx) * nx + (s1.vy - s2.vy) * ny;
            if (dvn < 0) continue;
            s1.vx -= dvn * nx; s1.vy -= dvn * ny;
            s2.vx += dvn * nx; s2.vy += dvn * ny;
            const ov = (md - d) / 2;
            s1.x -= nx * ov; s1.y -= ny * ov;
            s2.x += nx * ov; s2.y += ny * ov;
          }
        }
      }
    }

    // Trail
    if (this._steps % 2 === 0) {
      this.trail.push({ x: big.x, y: big.y });
      if (this.trail.length > 600) this.trail.shift();
    }

    // MSD history
    if (this._steps % 6 === 0) {
      const dx = big.x - this._origin.x, dy = big.y - this._origin.y;
      this._msdHistory.push({ step: this._steps, msd: dx * dx + dy * dy });
      if (this._msdHistory.length > 250) this._msdHistory.shift();
    }

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

  info() {
    const dx = this.big.x - this._origin.x, dy = this.big.y - this._origin.y;
    return {
      steps:        this._steps,
      displacement: Math.hypot(dx, dy).toFixed(1),
      msd:          (dx * dx + dy * dy).toFixed(0),
      speed:        Math.hypot(this.big.vx, this.big.vy).toFixed(2),
      N: this.N, T: this.T,
    };
  }

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

    const bg = ctx.createRadialGradient(W / 2, H / 2, 0, W / 2, H / 2, Math.max(W, H) * 0.7);
    bg.addColorStop(0, '#080818'); bg.addColorStop(1, '#03030C');
    ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);

    // Dot grid
    ctx.fillStyle = 'rgba(255,255,255,0.025)';
    for (let x = 0; x < W; x += 30) for (let y = 0; y < H; y += 30) {
      ctx.beginPath(); ctx.arc(x, y, 1, 0, TAU); ctx.fill();
    }

    // MSD history chart (bottom-left)
    this._drawMsdChart(ctx, W, H);

    // Age-gradient trail
    const trail = this.trail;
    for (let i = 1; i < trail.length; i++) {
      const frac = i / trail.length;
      // young <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> gold (#FFD166), old <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> dark indigo
      const hue = 220 + (1 - frac) * 20;   // 220..240 — indigo <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> blue
      const sat = 60 + frac * 40;
      const lit = 20 + frac * 50;
      ctx.beginPath();
      ctx.arc(trail[i].x, trail[i].y, 1.5, 0, TAU);
      ctx.fillStyle = `hsla(${hue},${sat}%,${lit}%,${frac * 0.75})`;
      ctx.fill();
    }
    // Newest segment in gold
    if (trail.length > 2) {
      const t0 = trail[trail.length - 2], t1 = trail[trail.length - 1];
      ctx.strokeStyle = 'rgba(255,209,102,0.9)'; ctx.lineWidth = 2;
      ctx.beginPath(); ctx.moveTo(t0.x, t0.y); ctx.lineTo(t1.x, t1.y); ctx.stroke();
    }

    // Displacement vector
    const ox = this._origin.x, oy = this._origin.y;
    const bx = this.big.x,    by = this.big.y;
    const vlen = Math.hypot(bx - ox, by - oy);
    if (vlen > 2) {
      ctx.save();
      ctx.strokeStyle = 'rgba(255,100,100,0.6)'; ctx.lineWidth = 1.5;
      ctx.beginPath(); ctx.moveTo(ox, oy); ctx.lineTo(bx, by); ctx.stroke();
      const ang = Math.atan2(by - oy, bx - ox), hl = 8;
      ctx.fillStyle = 'rgba(255,100,100,0.6)';
      ctx.beginPath(); ctx.moveTo(bx, by);
      ctx.lineTo(bx - hl * Math.cos(ang - 0.4), by - hl * Math.sin(ang - 0.4));
      ctx.lineTo(bx - hl * Math.cos(ang + 0.4), by - hl * Math.sin(ang + 0.4));
      ctx.closePath(); ctx.fill();
      const mx = (ox + bx) / 2, my = (oy + by) / 2;
      ctx.fillStyle = 'rgba(255,140,140,0.85)';
      ctx.font = "10px 'Manrope', sans-serif";
      ctx.fillText(`|Δr| = ${vlen.toFixed(1)}`, mx + 6, my - 4);
      ctx.restore();
    }

    // Origin marker
    ctx.save();
    ctx.strokeStyle = 'rgba(255,100,100,0.35)'; ctx.lineWidth = 1;
    ctx.setLineDash([3, 3]);
    ctx.beginPath(); ctx.arc(ox, oy, 6, 0, TAU); ctx.stroke();
    ctx.setLineDash([]);
    ctx.restore();

    // Small particles
    ctx.save();
    ctx.shadowBlur = 4; ctx.shadowColor = '#4CC9F0'; ctx.fillStyle = '#4CC9F0';
    for (const s of this.small) {
      ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, TAU); ctx.fill();
    }
    ctx.restore();

    // Big particle
    const big = this.big;
    ctx.save();
    ctx.shadowBlur = 32; ctx.shadowColor = 'rgba(255,214,0,0.65)';
    const grad = ctx.createRadialGradient(
      big.x - big.r * 0.3, big.y - big.r * 0.3, 2,
      big.x, big.y, big.r
    );
    grad.addColorStop(0, '#FFD166'); grad.addColorStop(1, '#9B5DE5');
    ctx.fillStyle = grad;
    ctx.beginPath(); ctx.arc(big.x, big.y, big.r, 0, TAU); ctx.fill();

    // Hover ring
    if (this._hover) {
      ctx.shadowBlur = 0;
      ctx.strokeStyle = 'rgba(255,255,255,0.55)'; ctx.lineWidth = 2;
      ctx.beginPath(); ctx.arc(big.x, big.y, big.r + 4, 0, TAU); ctx.stroke();
    }

    ctx.shadowBlur = 0;
    ctx.strokeStyle = 'rgba(255,255,255,0.3)'; ctx.lineWidth = 1.5;
    ctx.beginPath(); ctx.arc(big.x, big.y, big.r, 0, TAU); ctx.stroke();

    ctx.fillStyle = 'white'; ctx.font = "bold 12px 'Manrope', sans-serif";
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('B', big.x, big.y);
    ctx.restore();

    // Hover tooltip
    if (this._hover) this._drawBigTooltip(ctx, W, H);
    if (window.LabFX) LabFX.particles.draw(ctx);
  }

  _drawMsdChart(ctx, W, H) {
    const hist = this._msdHistory;
    const chartW = 190, chartH = 90;
    const cx = 14, cy = H - chartH - 14;

    ctx.save();
    ctx.fillStyle = 'rgba(0,0,10,0.72)';
    ctx.beginPath(); ctx.roundRect(cx, cy, chartW, chartH, 8); ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.07)'; ctx.lineWidth = 1;
    ctx.stroke();

    ctx.fillStyle = 'rgba(255,255,255,0.6)'; ctx.font = "9px 'Manrope', sans-serif";
    ctx.textAlign = 'left'; ctx.textBaseline = 'top';
    ctx.fillText('MSD vs Шагов', cx + 8, cy + 7);

    if (hist.length > 2) {
      const padL = 8, padR = 10, padT = 20, padB = 8;
      const pw = chartW - padL - padR;
      const ph = chartH - padT - padB;
      const maxMsd = Math.max(...hist.map(h => h.msd), 1);

      ctx.strokeStyle = '#9B5DE5'; ctx.lineWidth = 1.5;
      ctx.beginPath();
      for (let i = 0; i < hist.length; i++) {
        const hx = cx + padL + (i / (hist.length - 1)) * pw;
        const hy = cy + padT + ph - (hist[i].msd / maxMsd) * ph;
        if (i === 0) ctx.moveTo(hx, hy); else ctx.lineTo(hx, hy);
      }
      ctx.stroke();

      // Theoretical linear MSD ~ D*t line (straight reference)
      const lastMsd  = hist[hist.length - 1].msd;
      const firstMsd = hist[0].msd;
      ctx.strokeStyle = 'rgba(255,209,102,0.5)'; ctx.lineWidth = 1;
      ctx.setLineDash([4, 4]);
      ctx.beginPath();
      ctx.moveTo(cx + padL, cy + padT + ph - (firstMsd / maxMsd) * ph);
      ctx.lineTo(cx + padL + pw, cy + padT + ph - (lastMsd / maxMsd) * ph);
      ctx.stroke();
      ctx.setLineDash([]);

      // Current value
      const last = hist[hist.length - 1];
      ctx.fillStyle = '#9B5DE5'; ctx.font = "bold 10px 'Manrope', sans-serif";
      ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
      ctx.fillText(last.msd.toFixed(0), cx + chartW - padR - 2,
        cy + padT + ph - (last.msd / maxMsd) * ph);
    } else {
      ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.font = "10px 'Manrope', sans-serif";
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText('Накапливается…', cx + chartW / 2, cy + chartH / 2);
    }

    ctx.restore();
  }

  _drawBigTooltip(ctx, W, H) {
    const big  = this.big;
    const spd  = Math.hypot(big.vx, big.vy);
    const ke   = 0.5 * big.r * big.r * spd * spd; // prop to mass (r²)
    const dx   = big.x - this._origin.x, dy = big.y - this._origin.y;
    const disp = Math.hypot(dx, dy);
    const msd  = dx * dx + dy * dy;

    const rows = [
      ['|v|',  spd.toFixed(2) + ' у.е.'],
      ['KE',   ke.toFixed(0)  + ' у.е.'],
      ['|Δr|', disp.toFixed(1) + ' px'],
      ['MSD',  msd.toFixed(0) + ' px²'],
    ];

    const tw = 142, th = 18 + rows.length * 17 + 8;
    let tx = big.x + big.r + 12, ty = big.y - th / 2;
    if (tx + tw > W - 10) tx = big.x - big.r - tw - 12;
    ty = Math.max(8, Math.min(H - th - 8, ty));

    ctx.save();
    ctx.fillStyle = 'rgba(6,8,28,0.93)';
    ctx.beginPath(); ctx.roundRect(tx, ty, tw, th, 8); ctx.fill();
    ctx.fillStyle = '#FFD166';
    ctx.beginPath(); ctx.roundRect(tx, ty, tw, 3, [8, 8, 0, 0]); ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.08)'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.roundRect(tx, ty, tw, th, 8); ctx.stroke();

    ctx.font = "11px 'Manrope', monospace"; ctx.textBaseline = 'middle';
    for (let i = 0; i < rows.length; i++) {
      const ry = ty + 18 + i * 17;
      ctx.fillStyle = 'rgba(255,255,255,0.42)'; ctx.textAlign = 'left';
      ctx.fillText(rows[i][0], tx + 10, ry);
      ctx.fillStyle = 'rgba(255,255,255,0.9)'; ctx.textAlign = 'right';
      ctx.fillText(rows[i][1], tx + tw - 10, ry);
    }
    ctx.restore();
  }
}

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