'use strict';
/* ══════════════════════════════════════════════════════════════
   GraphTransformSim — graph transformations explorer
   y = a·f(k·x + b) + c  with sliders for a, k, b, c
   Original f(x) shown faded, transformed shown bold.
   ══════════════════════════════════════════════════════════════ */

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

    /* base function */
    this._baseFn  = x => Math.sin(x);
    this._baseLabel = 'sin(x)';

    /* transform params */
    this.a = 1;
    this.k = 1;
    this.b = 0;
    this.c = 0;

    /* view */
    this.ox  = 0;
    this.oy  = 0;
    this.scl = 40;

    this.hx = null;
    this._drag = null;
    this.onUpdate = null;

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

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

  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 { a: this.a, k: this.k, b: this.b, c: this.c }; }
  setParams({ a, k, b, c } = {}) {
    if (a !== undefined) this.a = +a;
    if (k !== undefined) this.k = +k;
    if (b !== undefined) this.b = +b;
    if (c !== undefined) this.c = +c;
    this.draw();
    this._emit();
  }

  setBase(name) {
    const BASES = {
      'sin':   { fn: x => Math.sin(x),   label: 'sin(x)' },
      'cos':   { fn: x => Math.cos(x),   label: 'cos(x)' },
      'x^2':   { fn: x => x * x,         label: 'x²' },
      'sqrt':  { fn: x => x >= 0 ? Math.sqrt(x) : NaN, label: '√x' },
      '|x|':   { fn: x => Math.abs(x),   label: '|x|' },
      '1/x':   { fn: x => x !== 0 ? 1 / x : NaN, label: '1/x' },
      'x^3':   { fn: x => x * x * x,     label: 'x³' },
    };
    const b = BASES[name];
    if (b) { this._baseFn = b.fn; this._baseLabel = b.label; this.draw(); this._emit(); }
  }

  resetView() { this.ox = 0; this.oy = 0; this.scl = 40; this.draw(); }
  zoomIn()    { this.scl = Math.min(800, this.scl * 1.3); this.draw(); }
  zoomOut()   { this.scl = Math.max(4,   this.scl / 1.3); this.draw(); }

  info() {
    const { a, k, b, c } = this;
    const parts = [];
    if (a !== 1) parts.push(a === -1 ? '−' : a.toFixed(1) + '·');
    parts.push(this._baseLabel.replace('x', this._innerStr()));
    if (c > 0) parts.push(' + ' + c.toFixed(1));
    if (c < 0) parts.push(' − ' + Math.abs(c).toFixed(1));
    return {
      base: this._baseLabel,
      equation: 'y = ' + parts.join(''),
      a: a.toFixed(1),
      k: k.toFixed(1),
      b: b.toFixed(1),
      c: c.toFixed(1),
    };
  }

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

  _innerStr() {
    const { k, b } = this;
    let s = '';
    if (k !== 1) s += (k === -1 ? '−' : k.toFixed(1) + '·');
    s += 'x';
    if (b > 0) s += ' + ' + b.toFixed(1);
    if (b < 0) s += ' − ' + Math.abs(b).toFixed(1);
    return s;
  }

  _fBase(x) { try { return this._baseFn(x); } catch { return NaN; } }

  _fTransformed(x) {
    const inner = this.k * x + this.b;
    const base  = this._fBase(inner);
    return this.a * base + this.c;
  }

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

  /* ── coordinate transforms ────────────────────── */

  _toPx(mx, my) {
    return [
      this.W / 2 + (mx - this.ox) * this.scl,
      this.H / 2 - (my - this.oy) * this.scl,
    ];
  }

  _toMath(px, py) {
    return [
      (px - this.W / 2) / this.scl + this.ox,
      -(py - this.H / 2) / this.scl + this.oy,
    ];
  }

  /* ── 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);

    this._drawGrid(ctx, W, H);
    this._drawAxes(ctx, W, H);
    this._drawCurve(ctx, W, H, x => this._fBase(x), 'rgba(255,255,255,0.18)', 2); // original faded
    if (window.LabFX) {
      LabFX.glow.drawGlow(ctx, () => {
        this._drawCurve(ctx, W, H, x => this._fTransformed(x), '#9B5DE5', 2.5);
      }, { color: '#9B5DE5', intensity: 4 });
    } else {
      this._drawCurve(ctx, W, H, x => this._fTransformed(x), '#9B5DE5', 2.5);
    }
    this._drawEquation(ctx, W, H);
    if (this.hx !== null) this._drawHover(ctx, W, H);
  }

  _drawGrid(ctx, W, H) {
    const step = this._niceStep();
    const [x0] = this._toMath(0, 0), [x1] = this._toMath(W, 0);
    const [, y0] = this._toMath(0, H), [, y1] = this._toMath(0, 0);
    const gx = Math.floor(x0 / step) * step;
    const gy = Math.floor(y0 / step) * step;

    ctx.strokeStyle = 'rgba(255,255,255,0.065)';
    ctx.lineWidth = 1;
    for (let x = gx; x <= x1 + step; x += step) {
      const [px] = this._toPx(x, 0);
      ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, H); ctx.stroke();
    }
    for (let y = gy; y <= y1 + step; y += step) {
      const [, py] = this._toPx(0, y);
      ctx.beginPath(); ctx.moveTo(0, py); ctx.lineTo(W, py); ctx.stroke();
    }

    ctx.font = '11px Manrope, system-ui, sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.3)';
    const [axX, axY] = this._toPx(0, 0);
    const lblY = Math.max(4, Math.min(H - 18, axY + 5));
    const lblX = Math.max(28, Math.min(W - 6, axX - 5));

    ctx.textAlign = 'center'; ctx.textBaseline = 'top';
    for (let x = gx; x <= x1; x += step) {
      if (Math.abs(x) < step * 0.01) continue;
      const [px] = this._toPx(x, 0);
      if (px < 18 || px > W - 18) continue;
      ctx.fillText(this._fmtLabel(x, step), px, lblY);
    }
    ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
    for (let y = gy; y <= y1; y += step) {
      if (Math.abs(y) < step * 0.01) continue;
      const [, py] = this._toPx(0, y);
      if (py < 12 || py > H - 12) continue;
      ctx.fillText(this._fmtLabel(y, step), lblX, py);
    }
  }

  _niceStep() {
    const raw = this.W / this.scl / 8;
    const p = Math.pow(10, Math.floor(Math.log10(raw)));
    for (const m of [1, 2, 5, 10]) if (m * p >= raw) return m * p;
    return p;
  }

  _fmtLabel(n, step) {
    if (n === 0) return '0';
    if (step >= 1 && Number.isInteger(n)) return String(n);
    if (step < 0.001) return n.toExponential(1);
    const dec = Math.max(0, -Math.floor(Math.log10(step)));
    return n.toFixed(dec);
  }

  _drawAxes(ctx, W, H) {
    const [ax, ay] = this._toPx(0, 0);
    ctx.strokeStyle = 'rgba(255,255,255,0.4)';
    ctx.lineWidth = 1.5;
    ctx.beginPath(); ctx.moveTo(0, ay);  ctx.lineTo(W - 10, ay); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(ax, H); ctx.lineTo(ax, 8);      ctx.stroke();

    ctx.fillStyle = 'rgba(255,255,255,0.4)';
    const s = 5;
    // x arrow
    ctx.save(); ctx.translate(W - 8, ay); ctx.beginPath();
    ctx.moveTo(0, 0); ctx.lineTo(-s * 1.6, -s * 0.6); ctx.lineTo(-s * 1.6, s * 0.6); ctx.closePath(); ctx.fill(); ctx.restore();
    // y arrow
    ctx.save(); ctx.translate(ax, 6); ctx.rotate(-Math.PI / 2); ctx.beginPath();
    ctx.moveTo(0, 0); ctx.lineTo(-s * 1.6, -s * 0.6); ctx.lineTo(-s * 1.6, s * 0.6); ctx.closePath(); ctx.fill(); ctx.restore();

    ctx.fillStyle = 'rgba(255,255,255,0.55)';
    ctx.font = 'bold 12px Manrope, sans-serif';
    ctx.textBaseline = 'middle'; ctx.textAlign = 'left';
    ctx.fillText('x', W - 10, ay - 13);
    ctx.textBaseline = 'top'; ctx.textAlign = 'left';
    ctx.fillText('y', ax + 7, 4);
  }

  _drawCurve(ctx, W, H, fn, color, lw) {
    const steps = Math.min(W * 2, 2000);
    const [x0] = this._toMath(0, 0), [x1] = this._toMath(W, 0);
    const dx   = (x1 - x0) / steps;
    const maxJmp = (H / this.scl) * 2;

    ctx.strokeStyle = color;
    ctx.lineWidth = lw;
    ctx.lineJoin = 'round';
    ctx.beginPath();

    let pen = false, pyPrev = null;
    for (let i = 0; i <= steps; i++) {
      const mx = x0 + i * dx;
      const my = fn(mx);
      if (!isFinite(my) || isNaN(my)) { pen = false; pyPrev = null; continue; }
      if (pen && pyPrev !== null && Math.abs(my - pyPrev) > maxJmp) pen = false;
      const [px, py] = this._toPx(mx, my);
      pen ? ctx.lineTo(px, py) : ctx.moveTo(px, py);
      pen = true; pyPrev = my;
    }
    ctx.stroke();
  }

  _drawEquation(ctx, W, H) {
    const info = this.info();
    ctx.font = 'bold 13px Manrope, sans-serif';
    const text = info.equation;
    const tw = ctx.measureText(text).width;
    const x = W - tw - 24, y = 14;

    ctx.fillStyle = 'rgba(22,22,38,0.85)';
    ctx.beginPath(); ctx.roundRect(x, y, tw + 16, 26, 8); ctx.fill();
    ctx.strokeStyle = '#9B5DE5'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.roundRect(x, y, tw + 16, 26, 8); ctx.stroke();
    ctx.fillStyle = '#ddd';
    ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
    ctx.fillText(text, x + 8, y + 13);

    // base function label (faded)
    const base = 'f(x) = ' + this._baseLabel;
    ctx.font = '11px Manrope, sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.35)';
    ctx.fillText(base, x + 8, y + 38);
  }

  _drawHover(ctx, W, H) {
    const [px] = this._toPx(this.hx, 0);
    const myOrig  = this._fBase(this.hx);
    const myTrans = this._fTransformed(this.hx);

    ctx.strokeStyle = 'rgba(255,255,255,0.15)';
    ctx.lineWidth = 1;
    ctx.setLineDash([5, 5]);
    ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, H); ctx.stroke();
    ctx.setLineDash([]);

    // original point
    if (isFinite(myOrig)) {
      const [, py] = this._toPx(this.hx, myOrig);
      if (py > -20 && py < H + 20) {
        ctx.fillStyle = 'rgba(255,255,255,0.3)';
        ctx.beginPath(); ctx.arc(px, py, 4, 0, Math.PI * 2); ctx.fill();
      }
    }

    // transformed point
    if (isFinite(myTrans)) {
      const [, py2] = this._toPx(this.hx, myTrans);
      if (py2 > -20 && py2 < H + 20) {
        ctx.fillStyle = '#9B5DE5';
        ctx.beginPath(); ctx.arc(px, py2, 5, 0, Math.PI * 2); ctx.fill();
        ctx.strokeStyle = 'rgba(255,255,255,0.8)'; ctx.lineWidth = 1.5; ctx.stroke();
      }
    }
  }

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

  _bind() {
    const cv = this.canvas;

    cv.addEventListener('wheel', e => {
      e.preventDefault();
      const [mx, my] = this._toMath(e.offsetX, e.offsetY);
      this.scl = Math.max(4, Math.min(800, this.scl * (e.deltaY < 0 ? 1.15 : 1 / 1.15)));
      const [nx, ny] = this._toMath(e.offsetX, e.offsetY);
      this.ox -= nx - mx; this.oy -= ny - my;
      this.draw();
    }, { passive: false });

    cv.addEventListener('mousedown', e => {
      this._drag = { x: e.clientX, y: e.clientY, ox: this.ox, oy: this.oy };
      cv.style.cursor = 'grabbing';
    });
    window.addEventListener('mousemove', e => {
      if (this._drag) {
        this.ox = this._drag.ox - (e.clientX - this._drag.x) / this.scl;
        this.oy = this._drag.oy + (e.clientY - this._drag.y) / this.scl;
        this.draw();
      } else {
        const r = cv.getBoundingClientRect();
        const lx = e.clientX - r.left, ly = e.clientY - r.top;
        if (lx >= 0 && lx <= r.width && ly >= 0 && ly <= r.height) {
          this.hx = this._toMath(lx, ly)[0];
          this.draw();
        }
      }
    });
    window.addEventListener('mouseup', () => { this._drag = null; cv.style.cursor = 'crosshair'; });
    cv.addEventListener('mouseleave', () => { this.hx = null; this.draw(); });
    cv.style.cursor = 'crosshair';

    let t0 = null;
    cv.addEventListener('touchstart', e => {
      if (e.touches.length === 1)
        t0 = { x: e.touches[0].clientX, y: e.touches[0].clientY, ox: this.ox, oy: this.oy };
    }, { passive: true });
    cv.addEventListener('touchmove', e => {
      e.preventDefault();
      if (e.touches.length === 1 && t0) {
        this.ox = t0.ox - (e.touches[0].clientX - t0.x) / this.scl;
        this.oy = t0.oy + (e.touches[0].clientY - t0.y) / this.scl;
        this.draw();
      }
    }, { passive: false });
    cv.addEventListener('touchend', () => { t0 = null; });
  }
}

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

  function _openGraphTransform() {
    document.getElementById('sim-topbar-title').textContent = 'Трансформации графиков';
    _simShow('sim-graphtransform');
    _registerSimState('graphtransform', () => gtSim?.getParams(), st => gtSim?.setParams(st));
    if (_embedMode) _startStateEmit('graphtransform');
    requestAnimationFrame(() => requestAnimationFrame(() => {
      if (!gtSim) {
        gtSim = new GraphTransformSim(document.getElementById('graphtransform-canvas'));
        gtSim.onUpdate = _gtUpdateUI;
      }
      gtSim.fit();
      gtSim.draw();
      gtSim._emit();
    }));
  }

  let _gtSoundTs = 0;
  function gtParam(name, val) {
    const v = parseFloat(val);
    document.getElementById('gt-' + name + '-val').textContent = v % 1 === 0 ? v : v.toFixed(1);
    if (gtSim) gtSim.setParams({ [name]: v });
    const now = performance.now();
    if (window.LabFX && now - _gtSoundTs > 80) {
      _gtSoundTs = now;
      LabFX.sound.play('tick', { volume: 0.1 });
    }
  }

  function gtBase(name, btn) {
    document.querySelectorAll('.gt-base-btn').forEach(b => b.classList.remove('active'));
    if (btn) btn.classList.add('active');
    if (gtSim) gtSim.setBase(name);
  }

  function gtEffect(a, k, b, c) {
    document.getElementById('sl-gt-a').value = a; document.getElementById('gt-a-val').textContent = a;
    document.getElementById('sl-gt-k').value = k; document.getElementById('gt-k-val').textContent = k;
    document.getElementById('sl-gt-b').value = b; document.getElementById('gt-b-val').textContent = b;
    document.getElementById('sl-gt-c').value = c; document.getElementById('gt-c-val').textContent = c;
    if (gtSim) gtSim.setParams({ a, k, b, c });
    if (window.LabFX) LabFX.sound.play('whoosh', { volume: 0.3 });
  }

  function _gtUpdateUI(info) {
    const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
    v('gtbar-v1', info.base);
    v('gtbar-v2', info.a);
    v('gtbar-v3', info.k);
    v('gtbar-v4', info.b);
    v('gtbar-v5', info.c);
  }

  /* ── pendulum ── */
