'use strict';
/* ══════════════════════════════════════════════════════════════
   RefractionSim — light refraction simulation (Snell's law)
   n₁·sin(θ₁) = n₂·sin(θ₂)
   Total internal reflection · Fresnel coefficients · Dispersion
   Interactive incident ray drag · Presets
   ══════════════════════════════════════════════════════════════ */

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

    /* physics */
    this.n1    = 1.0;    // refractive index of top medium
    this.n2    = 1.5;    // refractive index of bottom medium
    this.angle = 30;     // incidence angle in degrees

    /* dispersion mode */
    this.dispersion = false;

    /* drag state */
    this._drag = false;

    /* 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 { n1: this.n1, n2: this.n2, angle: this.angle, dispersion: this.dispersion }; }
  setParams({ n1, n2, angle, dispersion } = {}) {
    if (n1    !== undefined) this.n1    = Math.max(1.0, Math.min(3.0, +n1));
    if (n2    !== undefined) this.n2    = Math.max(1.0, Math.min(3.0, +n2));
    if (angle !== undefined) this.angle = Math.max(0, Math.min(89, +angle));
    if (dispersion !== undefined) this.dispersion = !!dispersion;
    this.draw();
    this._emit();
  }

  reset() {
    this.n1 = 1.0; this.n2 = 1.5; this.angle = 30;
    this.dispersion = false;
    this.draw();
    this._emit();
  }

  info() {
    const { n1, n2, angle } = this;
    const theta1Rad = angle * Math.PI / 180;
    const sinTheta2 = (n1 / n2) * Math.sin(theta1Rad);
    const isTIR = Math.abs(sinTheta2) > 1;
    const criticalAngle = n1 > n2
      ? +(Math.asin(n2 / n1) * 180 / Math.PI).toFixed(1)
      : null;

    let angle2;
    if (isTIR) {
      angle2 = 'ПВО';
    } else {
      angle2 = +(Math.asin(sinTheta2) * 180 / Math.PI).toFixed(1);
    }

    return {
      n1: +n1.toFixed(2),
      n2: +n2.toFixed(2),
      angle1: +angle.toFixed(1),
      angle2,
      criticalAngle,
      isTIR,
    };
  }

  /* ── presets ────────────────────────────────── */

  static PRESETS = {
    air_glass:   { n1: 1.0,  n2: 1.5,  angle: 30 },
    glass_air:   { n1: 1.5,  n2: 1.0,  angle: 30 },
    water_glass: { n1: 1.33, n2: 1.5,  angle: 30 },
    diamond:     { n1: 1.0,  n2: 2.42, angle: 45 },
  };

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

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

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

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

    const midY = H / 2;
    const hitX = W / 2;
    const hitY = midY;

    /* --- background: two media --- */
    // top medium (lighter)
    const gradTop = ctx.createLinearGradient(0, 0, 0, midY);
    gradTop.addColorStop(0, '#131328');
    gradTop.addColorStop(1, '#1a1a3a');
    ctx.fillStyle = gradTop;
    ctx.fillRect(0, 0, W, midY);

    // bottom medium (darker, denser feel)
    const gradBot = ctx.createLinearGradient(0, midY, 0, H);
    gradBot.addColorStop(0, '#0e1a2e');
    gradBot.addColorStop(1, '#0D0D1A');
    ctx.fillStyle = gradBot;
    ctx.fillRect(0, midY, W, H - midY);

    /* --- interface line with glow --- */
    ctx.save();
    ctx.shadowColor = 'rgba(155, 93, 229, 0.4)';
    ctx.shadowBlur = 12;
    ctx.strokeStyle = 'rgba(155, 93, 229, 0.5)';
    ctx.lineWidth = 2;
    ctx.beginPath(); ctx.moveTo(0, midY); ctx.lineTo(W, midY); ctx.stroke();
    ctx.restore();

    /* --- normal line (dashed vertical) --- */
    ctx.strokeStyle = 'rgba(255,255,255,0.15)';
    ctx.lineWidth = 1;
    ctx.setLineDash([6, 4]);
    ctx.beginPath(); ctx.moveTo(hitX, 0); ctx.lineTo(hitX, H); ctx.stroke();
    ctx.setLineDash([]);

    /* --- physics --- */
    const theta1Rad = this.angle * Math.PI / 180;
    const sinTheta2 = (this.n1 / this.n2) * Math.sin(theta1Rad);
    const isTIR = Math.abs(sinTheta2) > 1;

    /* Fresnel reflectance (simplified) */
    let R = 1;
    if (!isTIR) {
      const theta2Rad = Math.asin(sinTheta2);
      const cosT1 = Math.cos(theta1Rad);
      const cosT2 = Math.cos(theta2Rad);
      const rs = (this.n1 * cosT1 - this.n2 * cosT2) / (this.n1 * cosT1 + this.n2 * cosT2);
      R = rs * rs;
    }

    /* ray length (from edge to hit point) */
    const rayLen = Math.max(W, H) * 0.6;

    /* --- critical angle indicator --- */
    if (this.n1 > this.n2) {
      const critRad = Math.asin(this.n2 / this.n1);
      const critDx = Math.sin(critRad);
      const critDy = Math.cos(critRad);
      ctx.strokeStyle = 'rgba(255,209,102,0.25)';
      ctx.lineWidth = 1;
      ctx.setLineDash([4, 4]);
      // critical angle ray in top medium
      ctx.beginPath();
      ctx.moveTo(hitX, hitY);
      ctx.lineTo(hitX - critDx * rayLen * 0.5, hitY - critDy * rayLen * 0.5);
      ctx.stroke();
      ctx.setLineDash([]);
      // label
      ctx.font = '10px Manrope, system-ui, sans-serif';
      ctx.fillStyle = 'rgba(255,209,102,0.5)';
      ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
      const lblX = hitX - critDx * rayLen * 0.35 + 6;
      const lblY = hitY - critDy * rayLen * 0.35;
      ctx.fillText('θc=' + (critRad * 180 / Math.PI).toFixed(1) + '°', lblX, lblY);
    }

    if (this.dispersion && !isTIR) {
      this._drawDispersion(ctx, hitX, hitY, midY, theta1Rad, rayLen);
    } else {
      this._drawMainRays(ctx, hitX, hitY, midY, theta1Rad, sinTheta2, isTIR, R, rayLen);
    }

    /* --- angle arcs --- */
    this._drawAngleArcs(ctx, hitX, hitY, theta1Rad, sinTheta2, isTIR);

    /* --- medium labels --- */
    this._drawMediumLabels(ctx, W, H, midY);

    /* --- info box --- */
    this._drawInfoBox(ctx, isTIR, R);

    /* --- drag handle indicator (incident ray endpoint) --- */
    const incDx = Math.sin(theta1Rad);
    const incDy = Math.cos(theta1Rad);
    const handleX = hitX - incDx * rayLen * 0.55;
    const handleY = hitY - incDy * rayLen * 0.55;
    const grad = ctx.createRadialGradient(handleX, handleY, 0, handleX, handleY, 10);
    grad.addColorStop(0, 'rgba(155,93,229,0.4)');
    grad.addColorStop(1, 'rgba(155,93,229,0)');
    ctx.fillStyle = grad;
    ctx.beginPath(); ctx.arc(handleX, handleY, 10, 0, Math.PI * 2); ctx.fill();
  }

  _drawMainRays(ctx, hitX, hitY, midY, theta1Rad, sinTheta2, isTIR, R, rayLen) {
    const incDx = Math.sin(theta1Rad);
    const incDy = Math.cos(theta1Rad);

    /* incident ray */
    const incStartX = hitX - incDx * rayLen;
    const incStartY = hitY - incDy * rayLen;
    this._drawRay(ctx, incStartX, incStartY, hitX, hitY, '#9B5DE5', 2.5);
    this._drawArrowhead(ctx, hitX, hitY, Math.atan2(hitY - incStartY, hitX - incStartX), '#9B5DE5');

    /* reflected ray */
    const refDx = incDx;   // same x component
    const refDy = -incDy;  // flipped y
    const refEndX = hitX + refDx * rayLen;
    const refEndY = hitY + refDy * rayLen;  // goes up (refDy is negative of incDy)
    const refAlpha = isTIR ? 1.0 : Math.max(0.3, Math.sqrt(R));
    ctx.globalAlpha = refAlpha;
    this._drawRay(ctx, hitX, hitY, refEndX, refEndY, '#EF476F', 2.5);
    this._drawArrowhead(ctx, refEndX, refEndY, Math.atan2(refEndY - hitY, refEndX - hitX), '#EF476F');
    ctx.globalAlpha = 1;

    /* refracted ray */
    if (!isTIR) {
      const theta2Rad = Math.asin(sinTheta2);
      const refracDx = Math.sin(theta2Rad);
      const refracDy = Math.cos(theta2Rad);
      const refracEndX = hitX + refracDx * rayLen;
      const refracEndY = hitY + refracDy * rayLen;
      const T = 1 - R;
      ctx.globalAlpha = Math.max(0.3, Math.sqrt(T));
      this._drawRay(ctx, hitX, hitY, refracEndX, refracEndY, '#06D6E0', 2.5);
      this._drawArrowhead(ctx, refracEndX, refracEndY,
        Math.atan2(refracEndY - hitY, refracEndX - hitX), '#06D6E0');
      ctx.globalAlpha = 1;
    }
  }

  _drawDispersion(ctx, hitX, hitY, midY, theta1Rad, rayLen) {
    /* Cauchy dispersion: n(λ) = A + B/λ² */
    const spectral = [
      { name: 'red',    color: '#FF0000', wave: 656 },
      { name: 'orange', color: '#FF7F00', wave: 589 },
      { name: 'yellow', color: '#FFFF00', wave: 550 },
      { name: 'green',  color: '#00FF00', wave: 510 },
      { name: 'cyan',   color: '#00FFFF', wave: 475 },
      { name: 'blue',   color: '#0000FF', wave: 450 },
      { name: 'violet', color: '#8B00FF', wave: 400 },
    ];

    /* incident white ray */
    const incDx = Math.sin(theta1Rad);
    const incDy = Math.cos(theta1Rad);
    const incStartX = hitX - incDx * rayLen;
    const incStartY = hitY - incDy * rayLen;
    this._drawRay(ctx, incStartX, incStartY, hitX, hitY, '#FFFFFF', 2.5);
    this._drawArrowhead(ctx, hitX, hitY, Math.atan2(hitY - incStartY, hitX - incStartX), '#FFFFFF');

    /* Cauchy coefficients derived from base n2 */
    const A = this.n2 - 4500 / (550 * 550);
    const B = 4500;

    for (const s of spectral) {
      const n2w = A + B / (s.wave * s.wave);
      const sinT2 = (this.n1 / n2w) * Math.sin(theta1Rad);
      if (Math.abs(sinT2) > 1) continue;
      const t2 = Math.asin(sinT2);
      const dx = Math.sin(t2);
      const dy = Math.cos(t2);
      ctx.globalAlpha = 0.85;
      this._drawRay(ctx, hitX, hitY, hitX + dx * rayLen, hitY + dy * rayLen, s.color, 1.5);
      ctx.globalAlpha = 1;
    }

    /* reflected (white, partial) */
    const refDx = incDx;
    const refDy = -incDy;
    ctx.globalAlpha = 0.35;
    this._drawRay(ctx, hitX, hitY, hitX + refDx * rayLen * 0.7, hitY + refDy * rayLen * 0.7, '#FFFFFF', 1.5);
    ctx.globalAlpha = 1;
  }

  _drawRay(ctx, x1, y1, x2, y2, color, width) {
    ctx.strokeStyle = color;
    ctx.lineWidth = width;
    ctx.lineCap = 'round';
    ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();

    /* subtle glow */
    ctx.save();
    ctx.shadowColor = color;
    ctx.shadowBlur = 8;
    ctx.globalAlpha = 0.3;
    ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
    ctx.restore();
  }

  _drawArrowhead(ctx, x, y, angle, color) {
    const aLen = 10;
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.moveTo(x, y);
    ctx.lineTo(x - aLen * Math.cos(angle - 0.3), y - aLen * Math.sin(angle - 0.3));
    ctx.lineTo(x - aLen * Math.cos(angle + 0.3), y - aLen * Math.sin(angle + 0.3));
    ctx.closePath(); ctx.fill();
  }

  _drawAngleArcs(ctx, hitX, hitY, theta1Rad, sinTheta2, isTIR) {
    const arcR = 50;
    const font = '12px Manrope, system-ui, sans-serif';

    /* θ₁ arc (incidence angle, measured from normal = vertical up) */
    if (this.angle > 1) {
      ctx.strokeStyle = 'rgba(155,93,229,0.6)';
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      // normal points up from hit: angle = -π/2 in canvas coords
      // incident ray comes from upper-left
      // Arc from normal (straight up = -π/2) to incident ray direction
      const normAngle = -Math.PI / 2;
      const incAngle  = -Math.PI / 2 - theta1Rad;
      ctx.arc(hitX, hitY, arcR, Math.min(incAngle, normAngle), Math.max(incAngle, normAngle));
      ctx.stroke();

      // label
      ctx.font = font;
      ctx.fillStyle = '#9B5DE5';
      ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      const midA = normAngle - theta1Rad / 2;
      ctx.fillText(
        'θ₁=' + this.angle.toFixed(1) + '°',
        hitX + (arcR + 20) * Math.cos(midA),
        hitY + (arcR + 20) * Math.sin(midA)
      );
    }

    /* θ₂ arc (refraction angle, measured from normal = vertical down) */
    if (!isTIR && Math.abs(sinTheta2) <= 1) {
      const theta2Rad = Math.asin(sinTheta2);
      if (theta2Rad > 0.02) {
        ctx.strokeStyle = 'rgba(6,214,224,0.6)';
        ctx.lineWidth = 1.5;
        ctx.beginPath();
        const normDown = Math.PI / 2;
        const refAngle = Math.PI / 2 + theta2Rad;
        ctx.arc(hitX, hitY, arcR * 0.8, Math.min(normDown, refAngle), Math.max(normDown, refAngle));
        ctx.stroke();

        // label
        const angle2Deg = theta2Rad * 180 / Math.PI;
        ctx.font = font;
        ctx.fillStyle = '#06D6E0';
        ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
        const midA2 = normDown + theta2Rad / 2;
        ctx.fillText(
          'θ₂=' + angle2Deg.toFixed(1) + '°',
          hitX + (arcR * 0.8 + 20) * Math.cos(midA2),
          hitY + (arcR * 0.8 + 20) * Math.sin(midA2)
        );
      }
    }
  }

  _drawMediumLabels(ctx, W, H, midY) {
    ctx.font = '13px Manrope, system-ui, sans-serif';
    ctx.textBaseline = 'middle';

    /* top medium */
    ctx.fillStyle = 'rgba(155,93,229,0.6)';
    ctx.textAlign = 'left';
    ctx.fillText('n₁ = ' + this.n1.toFixed(2), 16, midY - 30);

    /* bottom medium */
    ctx.fillStyle = 'rgba(6,214,224,0.6)';
    ctx.fillText('n₂ = ' + this.n2.toFixed(2), 16, midY + 30);

    /* TIR badge */
    const theta1Rad = this.angle * Math.PI / 180;
    const sinT2 = (this.n1 / this.n2) * Math.sin(theta1Rad);
    if (Math.abs(sinT2) > 1) {
      ctx.font = 'bold 14px Manrope, system-ui, sans-serif';
      ctx.fillStyle = '#EF476F';
      ctx.textAlign = 'center';
      ctx.fillText('Полное внутреннее отражение (ПВО)', W / 2, midY + 60);
    }
  }

  _drawInfoBox(ctx, isTIR, R) {
    const boxW = 220, boxH = 72;
    const bx = this.W - boxW - 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.textAlign = 'left'; ctx.textBaseline = 'top';

    ctx.fillStyle = 'rgba(255,255,255,0.7)';
    ctx.fillText('n₁·sin(θ₁) = n₂·sin(θ₂)', bx + 10, by + 10);

    const info = this.info();
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    const a2str = info.isTIR ? 'ПВО' : info.angle2 + '°';
    ctx.fillText(`θ₁ = ${info.angle1}°   θ₂ = ${a2str}`, bx + 10, by + 28);

    const rPct = (R * 100).toFixed(1);
    const tPct = ((1 - R) * 100).toFixed(1);
    ctx.fillStyle = '#EF476F';
    ctx.fillText(`R = ${rPct}%`, bx + 10, by + 46);
    ctx.fillStyle = '#06D6E0';
    ctx.fillText(`T = ${isTIR ? '0' : tPct}%`, bx + 90, by + 46);

    if (info.criticalAngle !== null) {
      ctx.fillStyle = '#FFD166';
      ctx.fillText(`θc = ${info.criticalAngle}°`, bx + 160, by + 46);
    }
  }

  /* ── 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) => {
      /* Check if near the incident ray line (top half only) */
      const hitX = this.W / 2;
      const hitY = this.H / 2;
      if (my >= hitY) return false;
      /* distance from mouse to the hit point — if within top half, allow drag */
      const dx = mx - hitX;
      const dy = my - hitY;
      const dist = Math.hypot(dx, dy);
      return dist > 20 && dist < Math.max(this.W, this.H) * 0.6;
    };

    const angleFromMouse = (mx, my) => {
      const hitX = this.W / 2;
      const hitY = this.H / 2;
      const dx = mx - hitX;
      const dy = hitY - my; // flip: canvas y goes down, angle measured from vertical up
      // angle from vertical = atan2(|dx|, dy)
      const a = Math.atan2(Math.abs(dx), dy) * 180 / Math.PI;
      return Math.max(0, Math.min(89, a));
    };

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

    const onMove = (e) => {
      if (!this._drag) return;
      if (e.cancelable) e.preventDefault();
      const { mx, my } = getPos(e);
      this.angle = angleFromMouse(mx, my);
      this.draw();
      this._emit();
    };

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

    /* 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 _openRefraction() {
    document.getElementById('sim-topbar-title').textContent = 'Преломление света';
    _simShow('sim-refraction');
    _registerSimState('refraction', () => refrSim?.getParams(), st => refrSim?.setParams(st));
    if (_embedMode) _startStateEmit('refraction');
    requestAnimationFrame(() => requestAnimationFrame(() => {
      if (!refrSim) {
        refrSim = new RefractionSim(document.getElementById('refraction-canvas'));
        refrSim.onUpdate = _refrUpdateUI;
      }
      refrSim.fit();
      refrSim.draw();
      refrSim._emit();
    }));
  }

  function refrParam(name, val) {
    const v = parseFloat(val);
    const ids = { n1: 'refr-n1-val', n2: 'refr-n2-val', angle: 'refr-angle-val' };
    const el = document.getElementById(ids[name]);
    if (el) el.textContent = name === 'angle' ? v : v.toFixed(2);
    if (refrSim) refrSim.setParams({ [name]: v });
  }

  function refrPreset(n1, n2, angle) {
    document.getElementById('sl-refr-n1').value = n1; document.getElementById('refr-n1-val').textContent = n1.toFixed(2);
    document.getElementById('sl-refr-n2').value = n2; document.getElementById('refr-n2-val').textContent = n2.toFixed(2);
    document.getElementById('sl-refr-angle').value = angle; document.getElementById('refr-angle-val').textContent = angle;
    if (refrSim) refrSim.setParams({ n1, n2, angle });
  }

  function _refrUpdateUI(info) {
    const v = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val; };
    v('refrbar-v1', info.angle1 + '°');
    v('refrbar-v2', info.isTIR ? 'ПВО' : info.angle2 + '°');
    v('refrbar-v3', info.criticalAngle !== null ? info.criticalAngle + '°' : '—');
    v('refrbar-v4', info.isTIR ? 'Да' : 'Нет');
  }

  /* ── probability ── */

