'use strict';
/* ══════════════════════════════════════════════════════════════
   PendulumSim — simple pendulum simulation
   θ'' = -(g/L)sin(θ) − γ·θ'
   RK4 integration · energy bar · trail · phase portrait
   ══════════════════════════════════════════════════════════════ */

class PendulumSim {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx    = canvas.getContext('2d');
    this.W = 0; this.H = 0;

    /* physics */
    this.L     = 200;   // px length
    this.g     = 9.81;
    this.theta = Math.PI / 4;  // angle (rad)
    this.omega = 0;     // angular velocity
    this.damping = 0;   // damping coefficient γ

    /* animation */
    this.playing = false;
    this._raf    = null;
    this._lastTs = null;
    this.speed   = 1;

    /* trail */
    this._trail = []; // [{x, y, age}]
    this._maxTrail = 200;

    /* energy chart (bottom) */
    this._eHistory = []; // [{t, ke, pe}]
    this._tSim = 0;

    this.onUpdate = null;

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

  /* ── public API ─────────────────────────────── */

  fit() {
    const dpr = window.devicePixelRatio || 1;
    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;
  }

  getParams() {
    return { L: this.L, g: this.g, theta: +(this.theta * 180 / Math.PI).toFixed(3), damping: this.damping };
  }
  setParams({ L, g, theta, damping } = {}) {
    if (L       !== undefined) this.L       = +L;
    if (g       !== undefined) this.g       = +g;
    if (theta   !== undefined) { this.theta = +theta * Math.PI / 180; this.omega = 0; this._clearTrail(); }
    if (damping !== undefined) this.damping = +damping;
    this.draw();
    this._emit();
  }

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

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

  reset() {
    this.pause();
    this.theta = Math.PI / 4;
    this.omega = 0;
    this._tSim = 0;
    this._clearTrail();
    this._eHistory = [];
    this.draw();
    this._emit();
  }

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

  info() {
    const T    = 2 * Math.PI * Math.sqrt(this.L / (this.g * 100)); // L in px <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> approx
    const KE   = 0.5 * this.omega * this.omega * this.L * this.L;
    const PE   = this.g * 100 * this.L * (1 - Math.cos(this.theta));
    const total = KE + PE;
    return {
      angle:  (this.theta * 180 / Math.PI).toFixed(1) + '°',
      omega:  this.omega.toFixed(3) + ' рад/с',
      period: T.toFixed(2) + ' с',
      energy: total > 0 ? Math.round(KE / total * 100) + '% KE' : '—',
    };
  }

  /* ── internals ─────────────────────────────── */

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

  _clearTrail() { this._trail = []; }

  _tick() {
    if (!this.playing) return;
    this._raf = requestAnimationFrame(ts => {
      if (this._lastTs === null) this._lastTs = ts;
      const rawDt = Math.min((ts - this._lastTs) / 1000, 0.05);
      this._lastTs = ts;
      const dt = rawDt * this.speed;

      this._step(dt);
      this._tSim += dt;

      // trail
      const { bx, by } = this._bobPos();
      this._trail.push({ x: bx, y: by });
      if (this._trail.length > this._maxTrail) this._trail.shift();

      // energy history
      const KE = 0.5 * this.omega * this.omega * this.L * this.L;
      const PE = this.g * 100 * this.L * (1 - Math.cos(this.theta));
      this._eHistory.push({ t: this._tSim, ke: KE, pe: PE });
      if (this._eHistory.length > 300) this._eHistory.shift();

      this.draw();
      this._emit();
      this._tick();
    });
  }

  /* RK4 step for θ'' = -(g/L)sinθ - γ·ω */
  _step(dt) {
    const gL = this.g * 100 / this.L; // scale g for px units
    const c  = this.damping;

    const deriv = (th, om) => ({
      dth: om,
      dom: -gL * Math.sin(th) - c * om,
    });

    const k1 = deriv(this.theta, this.omega);
    const k2 = deriv(this.theta + k1.dth * dt / 2, this.omega + k1.dom * dt / 2);
    const k3 = deriv(this.theta + k2.dth * dt / 2, this.omega + k2.dom * dt / 2);
    const k4 = deriv(this.theta + k3.dth * dt,     this.omega + k3.dom * dt);

    this.theta += dt / 6 * (k1.dth + 2 * k2.dth + 2 * k3.dth + k4.dth);
    this.omega += dt / 6 * (k1.dom + 2 * k2.dom + 2 * k3.dom + k4.dom);
  }

  _bobPos() {
    const cx = this.W / 2;
    const cy = Math.min(this.H * 0.18, 80);
    return {
      px: cx,
      py: cy,
      bx: cx + this.L * Math.sin(this.theta),
      by: cy + this.L * Math.cos(this.theta),
    };
  }

  /* ── draw ──────────────────────────────────── */

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

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

    const { px, py, bx, by } = this._bobPos();

    // trail
    this._drawTrail(ctx);

    // support
    ctx.fillStyle = 'rgba(255,255,255,0.25)';
    ctx.fillRect(W / 2 - 30, py - 4, 60, 4);

    // string
    ctx.strokeStyle = 'rgba(255,255,255,0.6)';
    ctx.lineWidth = 2;
    ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(bx, by); ctx.stroke();

    // pivot
    ctx.fillStyle = '#666';
    ctx.beginPath(); ctx.arc(px, py, 5, 0, Math.PI * 2); ctx.fill();

    // bob
    const bobR = 18;
    ctx.fillStyle = '#9B5DE5';
    ctx.beginPath(); ctx.arc(bx, by, bobR, 0, Math.PI * 2); ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 2; ctx.stroke();

    // glow
    const grad = ctx.createRadialGradient(bx, by, 0, bx, by, bobR * 2);
    grad.addColorStop(0, 'rgba(155,93,229,0.25)');
    grad.addColorStop(1, 'rgba(155,93,229,0)');
    ctx.fillStyle = grad;
    ctx.beginPath(); ctx.arc(bx, by, bobR * 2, 0, Math.PI * 2); ctx.fill();

    // angle arc
    if (Math.abs(this.theta) > 0.02) {
      ctx.strokeStyle = 'rgba(6,214,224,0.5)';
      ctx.lineWidth = 1.5;
      const arcR = 40;
      const startAngle = Math.PI / 2;
      const endAngle   = Math.PI / 2 + this.theta;
      ctx.beginPath();
      ctx.arc(px, py, arcR, Math.min(startAngle, endAngle), Math.max(startAngle, endAngle));
      ctx.stroke();

      ctx.fillStyle = '#06D6E0';
      ctx.font = '12px Manrope, sans-serif';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      const labelAngle = startAngle + this.theta / 2;
      ctx.fillText(
        (this.theta * 180 / Math.PI).toFixed(1) + '°',
        px + (arcR + 16) * Math.cos(labelAngle),
        py + (arcR + 16) * Math.sin(labelAngle)
      );
    }

    // energy bar
    this._drawEnergyBar(ctx, W, H);

    // energy chart
    this._drawEnergyChart(ctx, W, H);
  }

  _drawTrail(ctx) {
    const n = this._trail.length;
    if (n < 2) return;
    for (let i = 1; i < n; i++) {
      const a = i / n * 0.6;
      ctx.strokeStyle = `rgba(155,93,229,${a})`;
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(this._trail[i - 1].x, this._trail[i - 1].y);
      ctx.lineTo(this._trail[i].x, this._trail[i].y);
      ctx.stroke();
    }
  }

  _drawEnergyBar(ctx, W, H) {
    const KE = 0.5 * this.omega * this.omega * this.L * this.L;
    const PE = this.g * 100 * this.L * (1 - Math.cos(this.theta));
    const total = KE + PE || 1;
    const bw = 160, bh = 14;
    const x = W - bw - 20, y = 20;

    ctx.fillStyle = 'rgba(22,22,38,0.85)';
    ctx.beginPath(); ctx.roundRect(x - 8, y - 6, bw + 16, bh + 32, 8); ctx.fill();

    // KE bar
    const kw = (KE / total) * bw;
    ctx.fillStyle = '#EF476F';
    ctx.beginPath(); ctx.roundRect(x, y, Math.max(2, kw), bh, 4); ctx.fill();

    // PE bar
    ctx.fillStyle = '#06D6E0';
    ctx.beginPath(); ctx.roundRect(x + kw, y, Math.max(2, bw - kw), bh, 4); ctx.fill();

    ctx.font = '10px Manrope, sans-serif';
    ctx.textBaseline = 'top';
    ctx.fillStyle = '#EF476F'; ctx.textAlign = 'left';
    ctx.fillText('KE ' + Math.round(KE / total * 100) + '%', x, y + bh + 4);
    ctx.fillStyle = '#06D6E0'; ctx.textAlign = 'right';
    ctx.fillText('PE ' + Math.round(PE / total * 100) + '%', x + bw, y + bh + 4);
  }

  _drawEnergyChart(ctx, W, H) {
    const data = this._eHistory;
    if (data.length < 2) return;

    const cw = Math.min(300, W * 0.4);
    const ch = 80;
    const cx = W - cw - 20;
    const cy = H - ch - 20;

    ctx.fillStyle = 'rgba(22,22,38,0.7)';
    ctx.beginPath(); ctx.roundRect(cx - 8, cy - 8, cw + 16, ch + 16, 8); ctx.fill();

    let maxE = 0;
    for (const d of data) maxE = Math.max(maxE, d.ke + d.pe);
    if (maxE < 0.01) return;

    // PE filled area
    ctx.fillStyle = 'rgba(6,214,224,0.2)';
    ctx.beginPath();
    ctx.moveTo(cx, cy + ch);
    for (let i = 0; i < data.length; i++) {
      const x = cx + (i / (data.length - 1)) * cw;
      const y = cy + ch - (data[i].pe / maxE) * ch;
      ctx.lineTo(x, y);
    }
    ctx.lineTo(cx + cw, cy + ch);
    ctx.closePath(); ctx.fill();

    // KE line
    ctx.strokeStyle = '#EF476F';
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    for (let i = 0; i < data.length; i++) {
      const x = cx + (i / (data.length - 1)) * cw;
      const y = cy + ch - (data[i].ke / maxE) * ch;
      i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
    }
    ctx.stroke();

    // total line
    ctx.strokeStyle = 'rgba(255,255,255,0.25)';
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 4]);
    ctx.beginPath();
    for (let i = 0; i < data.length; i++) {
      const x = cx + (i / (data.length - 1)) * cw;
      const y = cy + ch - ((data[i].ke + data[i].pe) / maxE) * ch;
      i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
    }
    ctx.stroke();
    ctx.setLineDash([]);

    // labels
    ctx.font = '10px Manrope, sans-serif';
    ctx.textBaseline = 'bottom';
    ctx.fillStyle = '#EF476F'; ctx.textAlign = 'left'; ctx.fillText('KE', cx + 2, cy);
    ctx.fillStyle = '#06D6E0'; ctx.textAlign = 'center'; ctx.fillText('PE', cx + 30, cy);
    ctx.fillStyle = 'rgba(255,255,255,0.4)'; ctx.textAlign = 'right'; ctx.fillText('Total', cx + cw, cy);
  }

  /* ── events ─────────────────────────────────── */

  _bindEvents() {
    const cv = this.canvas;

    cv.addEventListener('mousedown', e => {
      const { bx, by } = this._bobPos();
      const r = cv.getBoundingClientRect();
      const mx = (e.clientX - r.left) * (this.W / r.width);
      const my = (e.clientY - r.top)  * (this.H / r.height);
      if (Math.hypot(mx - bx, my - by) < 30) {
        this._drag = true;
        this.pause();
      }
    });

    window.addEventListener('mousemove', e => {
      if (!this._drag) return;
      const r = cv.getBoundingClientRect();
      const mx = (e.clientX - r.left) * (this.W / r.width);
      const my = (e.clientY - r.top)  * (this.H / r.height);
      const { px, py } = this._bobPos();
      this.theta = Math.atan2(mx - px, my - py);
      this.omega = 0;
      this._clearTrail();
      this.draw();
      this._emit();
    });

    window.addEventListener('mouseup', () => {
      if (this._drag) {
        this._drag = false;
        this.play();
      }
    });

    // touch
    cv.addEventListener('touchstart', e => {
      if (e.touches.length !== 1) return;
      const { bx, by } = this._bobPos();
      const r = cv.getBoundingClientRect();
      const mx = (e.touches[0].clientX - r.left) * (this.W / r.width);
      const my = (e.touches[0].clientY - r.top)  * (this.H / r.height);
      if (Math.hypot(mx - bx, my - by) < 40) {
        this._drag = true;
        this.pause();
      }
    }, { passive: true });
    cv.addEventListener('touchmove', e => {
      if (!this._drag) return;
      e.preventDefault();
      const r = cv.getBoundingClientRect();
      const mx = (e.touches[0].clientX - r.left) * (this.W / r.width);
      const my = (e.touches[0].clientY - r.top)  * (this.H / r.height);
      const { px, py } = this._bobPos();
      this.theta = Math.atan2(mx - px, my - py);
      this.omega = 0;
      this._clearTrail();
      this.draw();
      this._emit();
    }, { passive: false });
    cv.addEventListener('touchend', () => {
      if (this._drag) { this._drag = false; this.play(); }
    });
  }
}

/* ─── lab UI init ─────────────────────────────────── */
  var pendSim = null;

  function _openPendulum() {
    document.getElementById('sim-topbar-title').textContent = 'Маятник';
    _simShow('sim-pendulum');
    _registerSimState('pendulum', () => pendSim?.getParams(), st => pendSim?.setParams(st));
    if (_embedMode) _startStateEmit('pendulum');
    requestAnimationFrame(() => requestAnimationFrame(() => {
      if (!pendSim) {
        pendSim = new PendulumSim(document.getElementById('pendulum-canvas'));
        pendSim.onUpdate = _pendUpdateUI;
      }
      pendSim.fit();
      pendSim.play();
    }));
  }

  function pendParam(name, val) {
    const v = parseFloat(val);
    const ids = { theta: 'pend-theta-val', L: 'pend-L-val', g: 'pend-g-val', damping: 'pend-damp-val' };
    const el = document.getElementById(ids[name]);
    if (el) el.textContent = v % 1 === 0 ? v : v.toFixed(name === 'g' ? 2 : 1);
    if (pendSim) pendSim.setParams({ [name]: v });
  }

  function pendPreset(theta, L, g, damp) {
    document.getElementById('sl-pend-theta').value = theta; document.getElementById('pend-theta-val').textContent = theta;
    document.getElementById('sl-pend-L').value = L;         document.getElementById('pend-L-val').textContent = L;
    document.getElementById('sl-pend-g').value = g;         document.getElementById('pend-g-val').textContent = g;
    document.getElementById('sl-pend-damp').value = damp;   document.getElementById('pend-damp-val').textContent = damp;
    if (pendSim) {
      pendSim.setParams({ theta, L, g, damping: damp });
      pendSim.play();
    }
  }

  function _pendUpdateUI(info) {
    const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
    v('pendbar-v1', info.angle);
    v('pendbar-v2', info.omega);
    v('pendbar-v3', info.period);
    v('pendbar-v4', info.energy);
  }

  /* ── equilibrium ── */

