/* chem7_anim.js — анимационный движок флагманов учебника «Химия 7».
 * Неймспейс window.Chem7Anim. Используется виджетами chem7_chN_widgets.js.
 *
 * Принципы (см. plans/textbooks-7/PLAN_CHEMISTRY_7_VISUAL.md):
 *  - один RAF-цикл на флагман, пауза вне вьюпорта (IntersectionObserver), stop() при уходе;
 *  - prefers-reduced-motion → статичный кадр вместо цикла;
 *  - тёмная тема через CSS-переменные; без эмодзи;
 *  - молекулы — SVG (надёжно в jsdom); частицы/пламя/пузырьки — canvas, но в headless
 *    (jsdom-тесты) getContext НЕ вызывается, строится только DOM-каркас.
 */
(function (W) {
  'use strict';
  var D = W.document;
  var HEADLESS = (typeof navigator !== 'undefined' && /jsdom|HeadlessChrome/i.test(navigator.userAgent || ''));
  function reduced() {
    try { return !!(W.matchMedia && W.matchMedia('(prefers-reduced-motion: reduce)').matches); } catch (e) { return false; }
  }
  function ease(t) { return t < .5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
  function now() { try { return W.performance && W.performance.now ? W.performance.now() : Date.now(); } catch (e) { return Date.now(); } }
  function rand(a, b) { return a + Math.random() * (b - a); }

  /* наблюдатель видимости (пауза вне экрана); в jsdom IO нет → всегда видим */
  function observeVisible(host, cb) {
    if (typeof IntersectionObserver === 'undefined') { cb(true); return { disconnect: function () {} }; }
    var io = new IntersectionObserver(function (es) { cb(es[0] && es[0].isIntersecting); }, { threshold: 0.01 });
    io.observe(host); return io;
  }

  /* RAF-цикл с паузой вне экрана. step(dt, tSec). Возвращает {stop()}. */
  function loop(host, step, opts) {
    opts = opts || {};
    var raf = 0, running = true, visible = true, last = now(), t0 = last;
    if (reduced() || HEADLESS) { try { step(0, 0); } catch (e) {} return { stop: function () {} }; }
    var io = observeVisible(host, function (v) { visible = v; });
    function frame() {
      if (!running) return;
      var t = now(), dt = Math.min(0.05, (t - last) / 1000); last = t;
      if (visible) { try { step(dt, (t - t0) / 1000); } catch (e) { running = false; return; } }
      raf = W.requestAnimationFrame(frame);
    }
    raf = W.requestAnimationFrame(frame);
    return { stop: function () { running = false; try { W.cancelAnimationFrame(raf); } catch (e) {} try { io.disconnect(); } catch (e) {} } };
  }

  /* создать canvas в host; ctx=null в headless (getContext не зовём) */
  function sceneCanvas(host, w, h) {
    host.innerHTML = '';
    var cv = D.createElement('canvas');
    var dpr = HEADLESS ? 1 : (W.devicePixelRatio || 1);
    cv.width = w * dpr; cv.height = h * dpr;
    cv.style.width = '100%'; cv.style.maxWidth = w + 'px'; cv.style.height = 'auto';
    cv.style.borderRadius = '12px'; cv.style.display = 'block';
    host.appendChild(cv);
    var ctx = null;
    if (!HEADLESS) { try { ctx = cv.getContext('2d'); if (ctx) ctx.scale(dpr, dpr); } catch (e) { ctx = null; } }
    return { cv: cv, ctx: ctx, w: w, h: h };
  }

  function cssVar(name, fallback) {
    try { var v = getComputedStyle(D.documentElement).getPropertyValue(name).trim(); return v || fallback; } catch (e) { return fallback; }
  }

  /* ---- 3D-молекула (SVG, depth-sorted, авто-вращение + drag) ---- */
  var ELEM = {
    H: { c: '#e2e8f0', r: 0.32 }, O: { c: '#ef4444', r: 0.46 }, N: { c: '#3b82f6', r: 0.45 },
    C: { c: '#334155', r: 0.46 }, S: { c: '#eab308', r: 0.52 }, Cl: { c: '#22c55e', r: 0.5 }
  };
  function molecule3d(host, spec) {
    var W0 = spec.size || 240, H0 = spec.size || 200, K = (spec.scale || 52);
    var ns = 'http://www.w3.org/2000/svg';
    host.innerHTML = '';
    var svg = D.createElementNS(ns, 'svg');
    svg.setAttribute('viewBox', '0 0 ' + W0 + ' ' + H0);
    svg.setAttribute('width', '100%');
    svg.style.maxWidth = W0 + 'px'; svg.style.height = 'auto'; svg.style.touchAction = 'none'; svg.style.cursor = 'grab';
    // defs: радиальные градиенты по элементам
    var defs = D.createElementNS(ns, 'defs'); var used = {};
    spec.atoms.forEach(function (a) { used[a.el] = 1; });
    Object.keys(used).forEach(function (el) {
      var col = (ELEM[el] || { c: '#94a3b8' }).c;
      var g = D.createElementNS(ns, 'radialGradient');
      g.setAttribute('id', 'g_' + el); g.setAttribute('cx', '35%'); g.setAttribute('cy', '32%'); g.setAttribute('r', '70%');
      g.innerHTML = '<stop offset="0%" stop-color="#fff" stop-opacity="0.92"/><stop offset="38%" stop-color="' + col + '"/><stop offset="100%" stop-color="' + col + '" stop-opacity="0.78"/>';
      defs.appendChild(g);
    });
    svg.appendChild(defs);
    var grp = D.createElementNS(ns, 'g'); svg.appendChild(grp);
    host.appendChild(svg);

    var ay = spec.startY != null ? spec.startY : 0.5, ax = -0.35;
    function render(angY, angX) {
      var cy0 = Math.cos(angY), sy0 = Math.sin(angY), cx0 = Math.cos(angX), sx0 = Math.sin(angX);
      var pts = spec.atoms.map(function (a, i) {
        var x = a.x * cy0 + a.z * sy0, z1 = -a.x * sy0 + a.z * cy0;
        var y = a.y * cx0 - z1 * sx0, z = a.y * sx0 + z1 * cx0;
        var depth = (z + 2) / 4; // 0..1
        return { i: i, el: a.el, sx: W0 / 2 + x * K, sy: H0 / 2 - y * K, z: z, depth: depth };
      });
      var order = pts.slice().sort(function (p, q) { return p.z - q.z; });
      var html = '';
      // связи (рисуем между центрами, под атомами по глубине ближнего конца)
      (spec.bonds || []).forEach(function (b) {
        var p = pts[b[0]], q = pts[b[1]];
        html += '<line x1="' + p.sx.toFixed(1) + '" y1="' + p.sy.toFixed(1) + '" x2="' + q.sx.toFixed(1) + '" y2="' + q.sy.toFixed(1) + '" stroke="#94a3b8" stroke-width="' + (5 + 3 * (p.depth + q.depth) / 2).toFixed(1) + '" stroke-linecap="round" opacity="0.55"/>';
      });
      order.forEach(function (p) {
        var er = (ELEM[p.el] || { r: 0.42 }).r, r = er * K * (0.72 + 0.5 * p.depth);
        html += '<circle cx="' + p.sx.toFixed(1) + '" cy="' + p.sy.toFixed(1) + '" r="' + r.toFixed(1) + '" fill="url(#g_' + p.el + ')" opacity="' + (0.6 + 0.4 * p.depth).toFixed(2) + '"/>';
        if (r > 9) html += '<text x="' + p.sx.toFixed(1) + '" y="' + (p.sy + r * 0.32).toFixed(1) + '" text-anchor="middle" font-size="' + (r * 0.85).toFixed(1) + '" font-weight="700" fill="#fff" opacity="' + (0.5 + 0.5 * p.depth).toFixed(2) + '">' + p.el + '</text>';
      });
      grp.innerHTML = html;
    }
    render(ay, ax);
    // drag-вращение (без setPointerCapture — правило проекта)
    var dragging = false, lx = 0, ly = 0;
    function down(e) { dragging = true; svg.style.cursor = 'grabbing'; var p = pt(e); lx = p.x; ly = p.y; }
    function move(e) { if (!dragging) return; var p = pt(e); ay += (p.x - lx) * 0.01; ax += (p.y - ly) * 0.01; lx = p.x; ly = p.y; render(ay, ax); }
    function up() { dragging = false; svg.style.cursor = 'grab'; }
    function pt(e) { var t = e.touches && e.touches[0] ? e.touches[0] : e; return { x: t.clientX || 0, y: t.clientY || 0 }; }
    svg.addEventListener('pointerdown', down);
    W.addEventListener('pointermove', move); W.addEventListener('pointerup', up);
    var h = loop(host, function (dt) { if (!dragging) { ay += dt * 0.6; render(ay, ax); } });
    return { stop: function () { h.stop(); try { W.removeEventListener('pointermove', move); W.removeEventListener('pointerup', up); } catch (e) {} } };
  }

  /* ---- сцена разделения смесей (canvas) ---- */
  function separation(host, kind) {
    var w = 300, h = 200, sc = sceneCanvas(host, w, h), ctx = sc.ctx;
    if (!ctx) return { stop: function () {} }; // headless: только каркас
    var P = [];
    function reset() {
      P = [];
      if (kind === 'magnet') {
        for (var i = 0; i < 60; i++) P.push({ x: rand(40, 260), y: rand(40, 150), iron: i % 2 === 0, vx: 0, vy: 0 });
      } else if (kind === 'filter') {
        for (var j = 0; j < 50; j++) P.push({ x: rand(120, 180), y: rand(10, 60), sand: j % 2 === 0, vy: rand(20, 50), settled: false });
      } else if (kind === 'evaporate') {
        for (var k = 0; k < 28; k++) P.push({ x: rand(90, 210), y: rand(120, 150), vy: -rand(12, 26), life: rand(0, 1) });
      } else if (kind === 'settle') {
        for (var m = 0; m < 60; m++) P.push({ x: rand(70, 230), y: rand(60, 150), oil: m % 2 === 0, vy: 0 });
      } else { // distill
        for (var n = 0; n < 24; n++) P.push({ x: rand(60, 90), y: rand(120, 150), vy: -rand(14, 26), phase: 0 });
      }
    }
    reset();
    var pri = cssVar('--pri', '#059669');
    function draw(dt, t) {
      ctx.clearRect(0, 0, w, h);
      if (kind === 'filter') {
        ctx.strokeStyle = '#94a3b8'; ctx.lineWidth = 2;
        ctx.beginPath(); ctx.moveTo(110, 30); ctx.lineTo(150, 110); ctx.lineTo(190, 30); ctx.stroke(); // воронка
        ctx.fillStyle = '#bfdbfe'; ctx.fillRect(120, 165, 60, 28); // стакан с водой
        P.forEach(function (p) {
          if (p.sand) { if (p.y < 100) p.y += p.vy * dt; ctx.fillStyle = '#a16207'; }
          else { p.y += p.vy * dt; if (p.y > 200) { p.y = 165; } ctx.fillStyle = '#3b82f6'; }
          ctx.beginPath(); ctx.arc(p.x, p.y, p.sand ? 2.5 : 2, 0, 7); ctx.fill();
        });
      } else if (kind === 'evaporate') {
        ctx.fillStyle = '#fde68a'; ctx.beginPath(); ctx.ellipse(150, 150, 70, 16, 0, 0, 7); ctx.fill(); // чашка
        // кристаллы соли растут
        var grow = Math.min(1, t / 6);
        for (var i = 0; i < 10; i++) { var s = 2 + grow * 5; ctx.fillStyle = '#e5e7eb'; ctx.fillRect(110 + i * 8, 150 - s, s, s); }
        P.forEach(function (p) { p.y += p.vy * dt; p.life += dt * 0.4; if (p.y < 20 || p.life > 1) { p.y = rand(140, 150); p.x = rand(100, 200); p.life = 0; }
          ctx.fillStyle = 'rgba(203,213,225,' + (0.6 * (1 - p.life)).toFixed(2) + ')'; ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, 7); ctx.fill(); });
      } else if (kind === 'magnet') {
        ctx.fillStyle = '#475569'; ctx.fillRect(135, 6, 30, 18); ctx.fillStyle = '#dc2626'; ctx.fillRect(135, 6, 15, 18); // магнит
        P.forEach(function (p) {
          if (p.iron) { var dx = 150 - p.x, dy = 22 - p.y, d = Math.hypot(dx, dy) || 1; p.x += dx / d * 70 * dt; p.y += dy / d * 70 * dt; ctx.fillStyle = '#475569'; }
          else { ctx.fillStyle = '#eab308'; }
          ctx.beginPath(); ctx.arc(p.x, p.y, 2.6, 0, 7); ctx.fill();
        });
      } else if (kind === 'settle') {
        P.forEach(function (p) { var target = p.oil ? 80 : 150; p.y += (target - p.y) * Math.min(1, dt * 1.5); ctx.fillStyle = p.oil ? '#fbbf24' : '#3b82f6'; ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, 7); ctx.fill(); });
      } else {
        ctx.fillStyle = '#94a3b8'; ctx.fillRect(40, 150, 60, 10); ctx.fillRect(210, 150, 60, 30); // колба и приёмник
        P.forEach(function (p) { if (p.phase === 0) { p.y += p.vy * dt; if (p.y < 40) { p.phase = 1; } } else { p.x += 60 * dt; p.y += 30 * dt; if (p.x > 240) { p.x = rand(60, 90); p.y = rand(120, 150); p.phase = 0; } }
          ctx.fillStyle = p.phase === 0 ? 'rgba(148,163,184,.7)' : '#3b82f6'; ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, 7); ctx.fill(); });
      }
    }
    return loop(host, draw);
  }

  /* ---- плавная смена цвета (индикаторы, осадки) ---- */
  function colorMorph(el, toColor, ms) {
    if (!el) return; ms = ms || 600;
    el.style.transition = 'background-color ' + ms + 'ms ease, color ' + ms + 'ms ease';
    el.style.backgroundColor = toColor;
  }

  /* ---- лёгкое конфетти (SVG, без CDN) ---- */
  function confettiSmall(host) {
    if (HEADLESS || reduced() || !host) return;
    var cols = ['#fbbf24', '#34d399', '#60a5fa', '#f472b6', '#a78bfa'];
    var box = D.createElement('div'); box.style.cssText = 'position:absolute;inset:0;pointer-events:none;overflow:hidden';
    if (getComputedStyle(host).position === 'static') host.style.position = 'relative';
    host.appendChild(box);
    for (var i = 0; i < 18; i++) {
      var s = D.createElement('div');
      s.style.cssText = 'position:absolute;top:-8px;left:' + rand(5, 95) + '%;width:7px;height:7px;border-radius:2px;background:' + cols[i % cols.length] + ';opacity:.9;transition:transform 1.1s cubic-bezier(.3,.7,.3,1),opacity 1.1s';
      box.appendChild(s);
      (function (el) { W.requestAnimationFrame(function () { el.style.transform = 'translateY(' + rand(120, 220) + 'px) rotate(' + rand(180, 540) + 'deg)'; el.style.opacity = '0'; }); })(s);
    }
    setTimeout(function () { try { host.removeChild(box); } catch (e) {} }, 1300);
  }

  /* ---- CSS-анимации (jsdom-safe, без canvas): пузырьки, осадок, пламя, смена цвета ---- */
  function injectKeyframes() {
    if (D.getElementById('chem7-kf')) return;
    var st = D.createElement('style'); st.id = 'chem7-kf';
    st.textContent =
      '@keyframes c7-rise{0%{transform:translateY(0) scale(.6);opacity:0}15%{opacity:.9}100%{transform:translateY(-92px) scale(1);opacity:0}}'
      + '@keyframes c7-fall{0%{transform:translateY(-26px);opacity:0}18%{opacity:1}100%{transform:translateY(58px);opacity:.85}}'
      + '@keyframes c7-flick{0%,100%{transform:scaleY(1);opacity:.92}50%{transform:scaleY(1.18) translateY(-3px);opacity:1}}';
    (D.head || D.documentElement).appendChild(st);
  }
  function fieldHost(host, h) {
    host.innerHTML = ''; host.style.position = 'relative'; host.style.height = h + 'px';
    host.style.overflow = 'hidden'; host.style.borderRadius = '12px';
    return host;
  }
  // поток пузырьков газа снизу вверх
  function bubbleField(host, opts) {
    opts = opts || {}; injectKeyframes(); fieldHost(host, opts.h || 120);
    host.style.background = opts.bg || 'linear-gradient(180deg,var(--pri-soft),transparent)';
    var n = opts.count || 14, col = opts.color || 'rgba(255,255,255,.85)';
    for (var i = 0; i < n; i++) {
      var d = D.createElement('div'), sz = rand(5, 11);
      d.style.cssText = 'position:absolute;bottom:6px;left:' + rand(8, 92) + '%;width:' + sz + 'px;height:' + sz + 'px;border-radius:50%;background:' + col + ';border:1px solid rgba(0,0,0,.12);animation:c7-rise ' + rand(1.3, 2.4).toFixed(2) + 's linear ' + rand(0, 1.6).toFixed(2) + 's infinite';
      host.appendChild(d);
    }
    return { stop: function () { try { host.innerHTML = ''; host.style.height = ''; } catch (e) {} } };
  }
  // осадок: частицы падают и оседают слоем
  function precipField(host, opts) {
    opts = opts || {}; injectKeyframes(); fieldHost(host, opts.h || 120);
    host.style.background = opts.bg || 'linear-gradient(180deg,transparent,var(--pri-soft))';
    var n = opts.count || 16, col = opts.color || '#38bdf8';
    var sed = D.createElement('div'); sed.style.cssText = 'position:absolute;left:0;right:0;bottom:0;height:14px;background:' + col + ';opacity:.55;border-radius:0 0 12px 12px'; host.appendChild(sed);
    for (var i = 0; i < n; i++) {
      var d = D.createElement('div'), sz = rand(5, 9);
      d.style.cssText = 'position:absolute;top:8px;left:' + rand(8, 92) + '%;width:' + sz + 'px;height:' + sz + 'px;border-radius:50%;background:' + col + ';animation:c7-fall ' + rand(1.1, 2.0).toFixed(2) + 's ease-in ' + rand(0, 1.4).toFixed(2) + 's infinite';
      host.appendChild(d);
    }
    return { stop: function () { try { host.innerHTML = ''; host.style.height = ''; } catch (e) {} } };
  }
  // пламя (мерцающая капля-градиент)
  function flameBox(host, opts) {
    opts = opts || {}; injectKeyframes(); fieldHost(host, opts.h || 120);
    var col = opts.color || '#f97316';
    var f = D.createElement('div');
    f.style.cssText = 'position:absolute;left:50%;bottom:10px;transform:translateX(-50%);transform-origin:bottom center;width:46px;height:78px;border-radius:50% 50% 50% 50%/60% 60% 40% 40%;background:radial-gradient(circle at 50% 75%,#fde047,' + col + ' 60%,transparent 78%);animation:c7-flick .5s ease-in-out infinite';
    host.appendChild(f);
    if (opts.sparks) for (var i = 0; i < 8; i++) { var s = D.createElement('div'); s.style.cssText = 'position:absolute;bottom:14px;left:' + rand(38, 62) + '%;width:3px;height:3px;border-radius:50%;background:#fb923c;animation:c7-rise ' + rand(.8, 1.4).toFixed(2) + 's linear ' + rand(0, 1).toFixed(2) + 's infinite'; host.appendChild(s); }
    return { stop: function () { try { host.innerHTML = ''; host.style.height = ''; } catch (e) {} } };
  }
  // блок вещества с плавной сменой цвета (зелёный→чёрный и т.п.)
  function colorBlock(host, fromC, toC, label, ms) {
    fieldHost(host, 90); ms = ms || 1800;
    var b = D.createElement('div');
    b.style.cssText = 'position:absolute;inset:14px;border-radius:10px;background:' + fromC + ';transition:background ' + ms + 'ms ease;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;text-shadow:0 1px 2px rgba(0,0,0,.4)';
    b.textContent = label || '';
    host.appendChild(b);
    W.requestAnimationFrame(function () { W.requestAnimationFrame(function () { b.style.background = toC; }); });
    return { stop: function () { try { host.innerHTML = ''; host.style.height = ''; } catch (e) {} }, el: b };
  }

  /* ---- валентные «крючки»: атомы A и B с чёрточками-связями, соединяющимися (§9) ---- */
  function valenceLink(host, spec) {
    var ns = 'http://www.w3.org/2000/svg';
    var na = spec.a.n, nb = spec.b.n, va = spec.a.val, vb = spec.b.val, lcm = va * na;
    var W0 = 300, r = 17, lx = 50, rx = W0 - 50;
    var H0 = Math.max(na, nb, 1) * 50 + 20;
    function ycol(n, k) { var gap = 50, top = (H0 - (n - 1) * gap) / 2; return top + k * gap; }
    function spread(idx, val) { return (idx - (val - 1) / 2) * 9; }
    var colA = spec.a.color || '#6366f1', colB = spec.b.color || '#ef4444';
    var bonds = '';
    for (var t = 0; t < lcm; t++) {
      var la = Math.floor(t / va), rb = Math.floor(t / vb);
      var sy = ycol(na, la) + spread(t % va, va), ey = ycol(nb, rb) + spread(t % vb, vb);
      var sx = lx + r, ex = rx - r, len = Math.hypot(ex - sx, ey - sy);
      bonds += '<line x1="' + sx + '" y1="' + sy.toFixed(1) + '" x2="' + ex + '" y2="' + ey.toFixed(1) + '" stroke="#94a3b8" stroke-width="3" stroke-linecap="round" stroke-dasharray="' + len.toFixed(1) + '" stroke-dashoffset="' + (HEADLESS ? 0 : len.toFixed(1)) + '" style="transition:stroke-dashoffset .5s ease ' + (t * 0.08).toFixed(2) + 's"/>';
    }
    function atom(x, y, el, col) {
      return '<circle cx="' + x + '" cy="' + y.toFixed(1) + '" r="' + r + '" fill="' + col + '" stroke="rgba(0,0,0,.2)"/>'
        + '<text x="' + x + '" y="' + (y + 4).toFixed(1) + '" text-anchor="middle" font-size="13" font-weight="700" fill="#fff">' + el + '</text>';
    }
    var atoms = '';
    for (var i = 0; i < na; i++) atoms += atom(lx, ycol(na, i), spec.a.el, colA);
    for (var j = 0; j < nb; j++) atoms += atom(rx, ycol(nb, j), spec.b.el, colB);
    host.innerHTML = '<svg viewBox="0 0 ' + W0 + ' ' + H0 + '" width="100%" style="max-width:' + W0 + 'px;height:auto">' + bonds + atoms + '</svg>';
    if (!HEADLESS && !reduced()) {
      var lines = host.querySelectorAll('line');
      W.requestAnimationFrame(function () { W.requestAnimationFrame(function () { lines.forEach(function (l) { l.setAttribute('stroke-dashoffset', '0'); }); }); });
    }
    return { stop: function () { try { host.innerHTML = ''; } catch (e) {} } };
  }

  W.Chem7Anim = {
    HEADLESS: HEADLESS, reduced: reduced, ease: ease, loop: loop, sceneCanvas: sceneCanvas,
    molecule3d: molecule3d, separation: separation, colorMorph: colorMorph, confettiSmall: confettiSmall, observeVisible: observeVisible,
    bubbleField: bubbleField, precipField: precipField, flameBox: flameBox, colorBlock: colorBlock, valenceLink: valenceLink
  };
})(window);
