'use strict';
/* ══════════════════════════════════════════════════════════════
   ThinLensSim — thin lens ray tracing simulation
   1/f = 1/d + 1/d'   M = -d'/d
   Three principal rays · draggable object & focal point
   Converging (f>0) and diverging (f<0) lenses
   ══════════════════════════════════════════════════════════════ */

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

    /* physics (px units) */
    this.f = 100;    // focal length
    this.d = 200;    // object distance (positive, measured from lens)
    this.h = 50;     // object height

    /* drag state */
    this._drag = null; // 'object' | 'focus' | null

    /* callback */
    this.onUpdate = 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 { f: this.f, d: this.d, h: this.h }; }
  setParams({ f, d, h } = {}) {
    if (f !== undefined) this.f = Math.max(-200, Math.min(200, +f));
    if (d !== undefined) this.d = Math.max(30, Math.min(400, +d));
    if (h !== undefined) this.h = Math.max(20, Math.min(80, +h));
    this.draw();
    this._emit();
  }

  reset() {
    this.f = 100; this.d = 200; this.h = 50;
    this.draw();
    this._emit();
  }

  info() {
    const { f, d, h } = this;
    const denom = d - f;
    const dPrime = Math.abs(denom) < 0.01 ? Infinity : (f * d) / denom;
    const M = Math.abs(denom) < 0.01 ? Infinity : -dPrime / d;
    const hPrime = M === Infinity ? Infinity : M * h;
    const isVirtual = dPrime < 0;
    return {
      f:    +f.toFixed(1),
      d:    +d.toFixed(1),
      dPrime: dPrime === Infinity ? Infinity : +dPrime.toFixed(1),
      M:      M === Infinity ? Infinity : +M.toFixed(3),
      imageType: isVirtual ? 'мнимое' : 'действительное',
      h:    +h.toFixed(1),
      hPrime: hPrime === Infinity ? Infinity : +Math.abs(hPrime).toFixed(1),
    };
  }

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

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

  /** Convert simulation coords to canvas coords.
   *  Origin = lens center; +x right, +y up.
   *  Canvas: lensX = W/2, axisY = H/2 */
  _toCanvas(sx, sy) {
    return { cx: this.W / 2 + sx, cy: this.H / 2 - sy };
  }

  _fromCanvas(cx, cy) {
    return { sx: cx - this.W / 2, sy: this.H / 2 - cy };
  }

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

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

    const { f, d, h } = this;
    const lensX = W / 2;
    const axisY = H / 2;

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

    /* optical axis */
    ctx.strokeStyle = 'rgba(255,255,255,0.15)';
    ctx.lineWidth = 1;
    ctx.setLineDash([6, 4]);
    ctx.beginPath(); ctx.moveTo(0, axisY); ctx.lineTo(W, axisY); ctx.stroke();
    ctx.setLineDash([]);

    /* lens */
    this._drawLens(ctx, lensX, axisY, f);

    /* focal & 2F points */
    this._drawFocalPoints(ctx, lensX, axisY, f);

    /* object arrow */
    const objX = lensX - d;
    this._drawArrow(ctx, objX, axisY, objX, axisY - h, '#9B5DE5', false);

    /* compute image */
    const denom = d - f;
    let dPrime, hPrime;
    if (Math.abs(denom) < 0.5) {
      /* object at focal point — rays parallel, no image */
      dPrime = null;
      hPrime = null;
    } else {
      dPrime = (f * d) / denom;
      const M = -dPrime / d;
      hPrime = M * h;
    }

    /* principal rays */
    this._drawRays(ctx, lensX, axisY, d, h, f, dPrime, hPrime);

    /* image arrow */
    if (dPrime !== null && isFinite(dPrime)) {
      const isVirtual = dPrime < 0;
      const imgX = lensX + dPrime;
      const imgTop = axisY - hPrime;
      this._drawArrow(ctx, imgX, axisY, imgX, imgTop,
        isVirtual ? '#FFD166' : '#EF476F', isVirtual);
    }

    /* labels */
    this._drawLabels(ctx, lensX, axisY, d, f, dPrime, hPrime);
  }

  _drawLens(ctx, lx, ay, f) {
    const lensH = Math.min(this.H * 0.38, 140);
    const converging = f > 0;

    ctx.strokeStyle = 'rgba(155,93,229,0.8)';
    ctx.lineWidth = 2.5;

    if (converging) {
      /* biconvex shape */
      const bulge = Math.min(18, Math.abs(f) * 0.12);
      ctx.beginPath();
      ctx.moveTo(lx, ay - lensH);
      ctx.quadraticCurveTo(lx + bulge, ay, lx, ay + lensH);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(lx, ay - lensH);
      ctx.quadraticCurveTo(lx - bulge, ay, lx, ay + lensH);
      ctx.stroke();
      /* arrowheads (converging) */
      this._lensArrow(ctx, lx, ay - lensH, -1);
      this._lensArrow(ctx, lx, ay + lensH, 1);
    } else {
      /* biconcave shape */
      const bulge = Math.min(14, Math.abs(f) * 0.1);
      ctx.beginPath();
      ctx.moveTo(lx, ay - lensH);
      ctx.quadraticCurveTo(lx - bulge, ay, lx, ay + lensH);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(lx, ay - lensH);
      ctx.quadraticCurveTo(lx + bulge, ay, lx, ay + lensH);
      ctx.stroke();
      /* arrowheads (diverging) */
      this._lensArrowDiv(ctx, lx, ay - lensH, -1);
      this._lensArrowDiv(ctx, lx, ay + lensH, 1);
    }

    /* center line */
    ctx.strokeStyle = 'rgba(155,93,229,0.3)';
    ctx.lineWidth = 1;
    ctx.beginPath(); ctx.moveTo(lx, ay - lensH); ctx.lineTo(lx, ay + lensH); ctx.stroke();
  }

  _lensArrow(ctx, x, y, dir) {
    const sz = 7;
    ctx.fillStyle = 'rgba(155,93,229,0.8)';
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x - sz, y + dir * sz * 1.2);
    ctx.lineTo(x + sz, y + dir * sz * 1.2);
    ctx.closePath(); ctx.fill();
  }

  _lensArrowDiv(ctx, x, y, dir) {
    const sz = 6;
    ctx.fillStyle = 'rgba(155,93,229,0.8)';
    ctx.beginPath();
    ctx.moveTo(x - sz, y);
    ctx.lineTo(x, y - dir * sz);
    ctx.lineTo(x + sz, y);
    ctx.closePath(); ctx.fill();
  }

  _drawFocalPoints(ctx, lx, ay, f) {
    const pts = [
      { sx:  f, label: "F'" },
      { sx: -f, label: 'F'  },
      { sx:  2 * f, label: "2F'" },
      { sx: -2 * f, label: '2F'  },
    ];

    for (const p of pts) {
      const px = lx + p.sx;
      if (px < 10 || px > this.W - 10) continue;
      const isFocal = !p.label.startsWith('2');
      const r = isFocal ? 5 : 3.5;
      const col = isFocal ? '#06D6E0' : 'rgba(6,214,224,0.5)';

      ctx.fillStyle = col;
      ctx.beginPath(); ctx.arc(px, ay, r, 0, Math.PI * 2); ctx.fill();

      ctx.font = '11px Manrope, system-ui, sans-serif';
      ctx.fillStyle = col;
      ctx.textAlign = 'center'; ctx.textBaseline = 'top';
      ctx.fillText(p.label, px, ay + 10);
    }
  }

  _drawArrow(ctx, x1, y1, x2, y2, color, dashed) {
    ctx.strokeStyle = color;
    ctx.fillStyle   = color;
    ctx.lineWidth   = 2.5;

    if (dashed) ctx.setLineDash([6, 4]);
    ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
    if (dashed) ctx.setLineDash([]);

    /* arrowhead */
    const angle = Math.atan2(y2 - y1, x2 - x1);
    const aLen = 10;
    ctx.beginPath();
    ctx.moveTo(x2, y2);
    ctx.lineTo(x2 - aLen * Math.cos(angle - 0.35), y2 - aLen * Math.sin(angle - 0.35));
    ctx.lineTo(x2 - aLen * Math.cos(angle + 0.35), y2 - aLen * Math.sin(angle + 0.35));
    ctx.closePath(); ctx.fill();
  }

  _drawRays(ctx, lx, ay, d, h, f, dPrime, hPrime) {
    const objX  = lx - d;
    const objY  = ay - h;
    const colors = ['#06D6E0', '#7BF5A4', '#FFD166'];
    const hasImage = dPrime !== null && isFinite(dPrime);
    const isVirtual = hasImage && dPrime < 0;

    ctx.lineWidth = 1.5;

    /* Ray 1: parallel to axis <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> through F' (converging) or from F' (diverging) */
    {
      ctx.strokeStyle = colors[0];
      ctx.setLineDash([]);
      /* incoming: object tip <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> lens, parallel */
      ctx.beginPath(); ctx.moveTo(objX, objY); ctx.lineTo(lx, objY); ctx.stroke();
      /* outgoing */
      if (hasImage) {
        const imgX = lx + dPrime;
        const imgY = ay - hPrime;
        if (!isVirtual) {
          ctx.beginPath(); ctx.moveTo(lx, objY); ctx.lineTo(imgX, imgY); ctx.stroke();
          /* extend past image */
          this._extendRay(ctx, lx, objY, imgX, imgY, colors[0]);
        } else {
          /* diverging outgoing ray + dashed virtual extension */
          const outSlope = (objY - ay) / f;
          ctx.beginPath(); ctx.moveTo(lx, objY);
          ctx.lineTo(lx + 300, objY + outSlope * 300); ctx.stroke();
          ctx.setLineDash([4, 4]);
          ctx.beginPath(); ctx.moveTo(lx, objY); ctx.lineTo(imgX, imgY); ctx.stroke();
          ctx.setLineDash([]);
        }
      }
    }

    /* Ray 2: through center <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> straight */
    {
      ctx.strokeStyle = colors[1];
      ctx.setLineDash([]);
      const slope = (objY - ay) / (objX - lx);
      const farX = lx + 350;
      const farY = ay + slope * 350;
      ctx.beginPath(); ctx.moveTo(objX, objY); ctx.lineTo(farX, farY); ctx.stroke();
      if (isVirtual) {
        /* extend behind lens too */
        const backX = lx - 350;
        const backY = ay - slope * 350;
        ctx.setLineDash([4, 4]);
        ctx.beginPath(); ctx.moveTo(lx, ay); ctx.lineTo(backX, backY); ctx.stroke();
        ctx.setLineDash([]);
      }
    }

    /* Ray 3: through F <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> parallel after lens */
    {
      ctx.strokeStyle = colors[2]; ctx.setLineDash([]);
      const fx = lx - f;
      const slope = (objY - ay) / (objX - fx);
      const hitY = objY + slope * (lx - objX);
      ctx.beginPath(); ctx.moveTo(objX, objY); ctx.lineTo(lx, hitY); ctx.stroke();
      const endX = hasImage && !isVirtual ? Math.max(lx + dPrime + 60, lx + 300) : lx + 300;
      ctx.beginPath(); ctx.moveTo(lx, hitY); ctx.lineTo(endX, hitY); ctx.stroke();
      if (hasImage && isVirtual) {
        ctx.setLineDash([4, 4]);
        ctx.beginPath(); ctx.moveTo(lx, hitY); ctx.lineTo(lx + dPrime, ay - hPrime); ctx.stroke();
        ctx.setLineDash([]);
      }
    }
  }

  _extendRay(ctx, x1, y1, x2, y2, color) {
    const dx = x2 - x1, dy = y2 - y1;
    const len = Math.hypot(dx, dy);
    if (len < 1) return;
    const ex = x2 + (dx / len) * 80;
    const ey = y2 + (dy / len) * 80;
    ctx.globalAlpha = 0.3;
    ctx.strokeStyle = color;
    ctx.beginPath(); ctx.moveTo(x2, y2); ctx.lineTo(ex, ey); ctx.stroke();
    ctx.globalAlpha = 1;
  }

  _drawLabels(ctx, lx, ay, d, f, dPrime, hPrime) {
    ctx.font = '12px Manrope, system-ui, sans-serif';
    ctx.textBaseline = 'top';

    /* d label */
    const objX = lx - d;
    ctx.fillStyle = '#9B5DE5';
    ctx.textAlign = 'center';
    ctx.fillText(`d = ${d.toFixed(0)}`, (objX + lx) / 2, ay + 26);

    /* f label */
    ctx.fillStyle = '#06D6E0';
    ctx.fillText(`f = ${f.toFixed(0)}`, lx, ay + 42);

    /* d' label */
    if (dPrime !== null && isFinite(dPrime)) {
      const imgX = lx + dPrime;
      ctx.fillStyle = dPrime > 0 ? '#EF476F' : '#FFD166';
      ctx.textAlign = 'center';
      ctx.fillText(`d' = ${dPrime.toFixed(1)}`, (lx + imgX) / 2, ay + 26);
    }

    /* formula box */
    const info = this.info();
    const boxW = 200, boxH = 52;
    const bx = 12, by = 12;
    ctx.fillStyle = 'rgba(22,22,38,0.85)';
    ctx.beginPath(); ctx.roundRect(bx, by, boxW, boxH, 8); ctx.fill();

    ctx.font = '11px Manrope, system-ui, sans-serif';
    ctx.fillStyle = 'rgba(255,255,255,0.7)';
    ctx.textAlign = 'left'; ctx.textBaseline = 'top';
    const mStr = info.M === Infinity ? '---' : info.M.toFixed(2);
    const dpStr = info.dPrime === Infinity ? '---' : info.dPrime.toFixed(1);
    ctx.fillText(`1/f = 1/d + 1/d'`, bx + 10, by + 10);
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.fillText(`M = ${mStr}   d' = ${dpStr}   ${info.imageType}`, bx + 10, by + 30);
  }

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

  _bindEvents() {
    const cv = this.canvas;

    const getPos = (e) => {
      const r = cv.getBoundingClientRect();
      const t = e.touches ? e.touches[0] : e;
      return {
        mx: (t.clientX - r.left) * (this.W / r.width),
        my: (t.clientY - r.top)  * (this.H / r.height),
      };
    };

    const hitTest = (mx, my) => {
      const lx = this.W / 2, ay = this.H / 2;
      /* object tip */
      const objX = lx - this.d;
      const objY = ay - this.h;
      if (Math.hypot(mx - objX, my - objY) < 20) return 'object';
      /* focal point F (front) */
      const fx = lx - this.f;
      if (Math.hypot(mx - fx, my - ay) < 16) return 'focus';
      return null;
    };

    const onDown = (e) => {
      const { mx, my } = getPos(e);
      this._drag = hitTest(mx, my);
    };

    const onMove = (e) => {
      if (!this._drag) return;
      if (e.cancelable) e.preventDefault();
      const { mx } = getPos(e);
      const lx = this.W / 2;

      if (this._drag === 'object') {
        this.d = Math.max(30, Math.min(400, lx - mx));
      } else if (this._drag === 'focus') {
        const newF = lx - mx;
        this.f = Math.max(-200, Math.min(200, newF));
      }
      this.draw();
      this._emit();
    };

    const onUp = () => { this._drag = null; };

    /* mouse */
    cv.addEventListener('mousedown', onDown);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);

    /* touch */
    cv.addEventListener('touchstart', e => {
      if (e.touches.length === 1) onDown(e);
    }, { passive: true });
    cv.addEventListener('touchmove', e => onMove(e), { passive: false });
    cv.addEventListener('touchend', onUp);

    /* cursor style */
    cv.addEventListener('mousemove', e => {
      if (this._drag) { cv.style.cursor = 'grabbing'; return; }
      const { mx, my } = getPos(e);
      cv.style.cursor = hitTest(mx, my) ? 'grab' : 'default';
    });
  }
}

/* ─── lab UI init ─────────────────────────────────── */
  function _openThinLens() {
    document.getElementById('sim-topbar-title').textContent = 'Тонкая линза';
    _simShow('sim-thinlens');
    _registerSimState('thinlens', () => lensSim?.getParams(), st => lensSim?.setParams(st));
    if (_embedMode) _startStateEmit('thinlens');
    requestAnimationFrame(() => requestAnimationFrame(() => {
      if (!lensSim) {
        lensSim = new ThinLensSim(document.getElementById('thinlens-canvas'));
        lensSim.onUpdate = _lensUpdateUI;
      }
      lensSim.fit();
      lensSim.draw();
      lensSim._emit();
    }));
  }

  function lensParam(name, val) {
    const v = parseFloat(val);
    const ids = { f: 'lens-f-val', d: 'lens-d-val', h: 'lens-h-val' };
    const el = document.getElementById(ids[name]);
    if (el) el.textContent = v;
    if (lensSim) lensSim.setParams({ [name]: v });
  }

  function lensPreset(f, d, h) {
    document.getElementById('sl-lens-f').value = f; document.getElementById('lens-f-val').textContent = f;
    document.getElementById('sl-lens-d').value = d; document.getElementById('lens-d-val').textContent = d;
    document.getElementById('sl-lens-h').value = h; document.getElementById('lens-h-val').textContent = h;
    if (lensSim) lensSim.setParams({ f, d, h });
  }

  function _lensUpdateUI(info) {
    const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
    v('lensbar-v1', info.f);
    v('lensbar-v2', info.dPrime === Infinity ? '∞' : info.dPrime);
    v('lensbar-v3', info.M === Infinity ? '∞' : info.M);
    v('lensbar-v4', info.imageType);
  }

  /* ── mirrors ── */

