'use strict';
/* ══════════════════════════════════════════════════════════
   LogicSim — Логические схемы
   Canvas-based digital logic circuit builder.
   Exports: LogicSim class, logicTool(), logicPreset(), logicClearAll(), _openLogic()
══════════════════════════════════════════════════════════ */

/* ── Gate definitions ── */
const GATE_DEFS = {
  INPUT:  { ins: 0, outs: 1, label: 'IN',    w: 56, h: 36 },
  CLOCK:  { ins: 0, outs: 1, label: 'CLK',   w: 56, h: 36 },
  OUTPUT: { ins: 1, outs: 0, label: 'OUT',   w: 56, h: 36 },
  AND:    { ins: 2, outs: 1, label: 'AND',   w: 64, h: 44 },
  OR:     { ins: 2, outs: 1, label: 'OR',    w: 64, h: 44 },
  NOT:    { ins: 1, outs: 1, label: 'NOT',   w: 56, h: 36 },
  XOR:    { ins: 2, outs: 1, label: 'XOR',   w: 64, h: 44 },
  NAND:   { ins: 2, outs: 1, label: 'NAND',  w: 64, h: 44 },
  NOR:    { ins: 2, outs: 1, label: 'NOR',   w: 64, h: 44 },
  XNOR:   { ins: 2, outs: 1, label: 'XNOR',  w: 64, h: 44 },
  BUFFER: { ins: 1, outs: 1, label: 'BUF',   w: 56, h: 36 },
};

const PORT_R = 5;         // port dot radius
const GRID   = 20;        // snap grid size

/* ── evaluate a single gate ── */
function evalGate(type, inputs) {
  const a = inputs[0] || 0;
  const b = inputs[1] || 0;
  switch (type) {
    case 'AND':    return a & b;
    case 'OR':     return a | b;
    case 'NOT':    return a ? 0 : 1;
    case 'XOR':    return a ^ b;
    case 'NAND':   return (a & b) ? 0 : 1;
    case 'NOR':    return (a | b) ? 0 : 1;
    case 'XNOR':   return (a ^ b) ? 0 : 1;
    case 'BUFFER': return a;
    case 'INPUT':  return a;   // value from state.value
    case 'CLOCK':  return a;
    case 'OUTPUT': return a;
    default:       return 0;
  }
}

/* ═══════════════════════════════════════════════════════════
   LogicSim
═══════════════════════════════════════════════════════════ */
class LogicSim {
  constructor(canvas, exprEl, tableEl) {
    this._canvas  = canvas;
    this._ctx     = canvas.getContext('2d');
    this._exprEl  = exprEl;   // element for boolean expression display
    this._tableEl = tableEl;  // element for truth table

    this._gates    = [];   // { id, type, x, y, value, label, freq, _phase }
    this._wires    = [];   // { from: {gateId, port:'out'|'in0'|'in1'}, to: {gateId, port} }
    this._nextId   = 1;

    this._tool     = 'select';  // 'select' | type key
    this._drag     = null;
    this._wireStart = null;  // { gateId, side:'out', px, py }

    this._history  = [];
    this._histIdx  = -1;

    this._raf      = null;
    this._clockRaf = null;
    this._fxLastT  = 0;

    this._bindEvents();
    this._startClock();
    this._startDrawLoop();
  }

  /* ── port pixel positions ── */
  _portPx(gate, port) {
    const def = GATE_DEFS[gate.type];
    const hw = def.w / 2, hh = def.h / 2;
    const cx = gate.x, cy = gate.y;
    if (port === 'out') return { x: cx + hw, y: cy };
    if (port === 'in0') {
      if (def.ins === 1) return { x: cx - hw, y: cy };
      return { x: cx - hw, y: cy - hh / 2 };
    }
    if (port === 'in1') return { x: cx - hw, y: cy + hh / 2 };
    return { x: cx, y: cy };
  }

  /* ── snap to grid ── */
  _snap(v) { return Math.round(v / GRID) * GRID; }

  /* ── find gate near point ── */
  _hitGate(px, py) {
    for (let i = this._gates.length - 1; i >= 0; i--) {
      const g = this._gates[i];
      const def = GATE_DEFS[g.type];
      if (Math.abs(px - g.x) <= def.w / 2 + 2 && Math.abs(py - g.y) <= def.h / 2 + 2) return g;
    }
    return null;
  }

  /* ── find port near point; returns { gateId, port, px, py } or null ── */
  _hitPort(px, py) {
    for (const g of this._gates) {
      const def = GATE_DEFS[g.type];
      const ports = [];
      if (def.outs > 0) ports.push('out');
      if (def.ins >= 1) ports.push('in0');
      if (def.ins >= 2) ports.push('in1');
      for (const port of ports) {
        const p = this._portPx(g, port);
        if (Math.hypot(px - p.x, py - p.y) <= 10) {
          return { gateId: g.id, port, px: p.x, py: p.y };
        }
      }
    }
    return null;
  }

  /* ── canvas coordinates from event ── */
  _pos(e) {
    const r = this._canvas.getBoundingClientRect();
    return { x: e.clientX - r.left, y: e.clientY - r.top };
  }

  /* ══ Event binding ══ */
  _bindEvents() {
    const c = this._canvas;
    c.addEventListener('mousedown',  this._onDown.bind(this));
    c.addEventListener('mousemove',  this._onMove.bind(this));
    c.addEventListener('mouseup',    this._onUp.bind(this));
    c.addEventListener('dblclick',   this._onDbl.bind(this));
    c.addEventListener('contextmenu', e => { e.preventDefault(); this._onRightClick(e); });
    window.addEventListener('keydown', e => {
      if (e.ctrlKey && e.key === 'z') { e.preventDefault(); this.undo(); }
      if (e.ctrlKey && e.key === 'y') { e.preventDefault(); this.redo(); }
    });
  }

  _onDown(e) {
    if (e.button !== 0) return;
    const { x, y } = this._pos(e);

    if (this._tool !== 'select') {
      // place a new gate
      const type = this._tool;
      const def  = GATE_DEFS[type];
      if (!def) return;
      this._pushHistory();
      const g = this._addGate(type, this._snap(x), this._snap(y));
      if (window.LabFX) LabFX.sound.play('tick', { pitch: 1.4, volume: 0.3 });
      this._propagate();
      this._updatePanels();
      this.draw();
      return;
    }

    // select tool: check port first (wire drawing)
    const hitP = this._hitPort(x, y);
    if (hitP && (hitP.port === 'out')) {
      this._wireStart = hitP;
      this._mouseX = x; this._mouseY = y;
      return;
    }

    // check gate drag
    const g = this._hitGate(x, y);
    if (g) {
      this._drag = { gate: g, ox: x - g.x, oy: y - g.y };
    }
  }

  _onMove(e) {
    const { x, y } = this._pos(e);
    this._mouseX = x; this._mouseY = y;

    if (this._drag) {
      this._drag.gate.x = this._snap(x - this._drag.ox);
      this._drag.gate.y = this._snap(y - this._drag.oy);
      this._propagate();
      this._updatePanels();
      this.draw();
      return;
    }
    if (this._wireStart) {
      this.draw();
      return;
    }
    // hover cursor
    const hp = this._hitPort(x, y);
    this._canvas.style.cursor = hp ? 'crosshair' : (this._hitGate(x, y) ? 'grab' : 'default');
  }

  _onUp(e) {
    if (this._drag) {
      this._pushHistory();
      this._drag = null;
      return;
    }
    if (this._wireStart) {
      const { x, y } = this._pos(e);
      const hitP = this._hitPort(x, y);
      if (hitP && (hitP.port === 'in0' || hitP.port === 'in1') && hitP.gateId !== this._wireStart.gateId) {
        // check not already wired
        const exists = this._wires.some(w => w.to.gateId === hitP.gateId && w.to.port === hitP.port);
        if (!exists) {
          this._pushHistory();
          this._wires.push({ from: { gateId: this._wireStart.gateId, port: this._wireStart.port }, to: { gateId: hitP.gateId, port: hitP.port } });
          if (window.LabFX) LabFX.sound.play('click', { pitch: 1.3 });
          this._propagate();
          this._updatePanels();
        }
      }
      this._wireStart = null;
      this.draw();
    }
  }

  _onDbl(e) {
    const { x, y } = this._pos(e);
    const g = this._hitGate(x, y);
    if (g && (g.type === 'INPUT')) {
      this._pushHistory();
      g.value = g.value ? 0 : 1;
      if (window.LabFX) {
        LabFX.sound.play('bounce', { pitch: 1.2 });
        LabFX.particles.emit({ ctx: this._ctx, x: g.x, y: g.y, count: 5, color: '#00ff88',
          speed: 20, spread: Math.PI * 2, angle: 0, gravity: 0, life: 400, fade: true,
          glow: true, shape: 'spark', size: 3, sizeFade: true });
      }
      this._propagate();
      this._updatePanels();
      this.draw();
    }
    if (g && g.type === 'OUTPUT') {
      // rename label cycle: OUT → OUT₁ → OUT₂ → OUT (no-op, just show)
    }
  }

  _onRightClick(e) {
    const { x, y } = this._pos(e);
    // delete wire near click
    for (let i = this._wires.length - 1; i >= 0; i--) {
      const w  = this._wires[i];
      const g1 = this._gateById(w.from.gateId); if (!g1) continue;
      const g2 = this._gateById(w.to.gateId);   if (!g2) continue;
      const p1 = this._portPx(g1, w.from.port);
      const p2 = this._portPx(g2, w.to.port);
      if (this._distToSeg(x, y, p1.x, p1.y, p2.x, p2.y) < 8) {
        this._pushHistory();
        this._wires.splice(i, 1);
        this._propagate();
        this._updatePanels();
        this.draw();
        return;
      }
    }
    // delete gate
    const g = this._hitGate(x, y);
    if (g) {
      this._pushHistory();
      this._wires = this._wires.filter(w => w.from.gateId !== g.id && w.to.gateId !== g.id);
      this._gates = this._gates.filter(gg => gg.id !== g.id);
      if (window.LabFX) LabFX.sound.play('fizz', { volume: 0.2 });
      this._propagate();
      this._updatePanels();
      this.draw();
    }
  }

  _distToSeg(px, py, ax, ay, bx, by) {
    const dx = bx - ax, dy = by - ay;
    const len2 = dx * dx + dy * dy;
    if (len2 === 0) return Math.hypot(px - ax, py - ay);
    const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / len2));
    return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
  }

  /* ══ Gate management ══ */
  _addGate(type, x, y) {
    const id  = this._nextId++;
    const def = GATE_DEFS[type];
    const g   = { id, type, x, y, value: 0, label: def.label };
    if (type === 'INPUT')  { g.label = String.fromCharCode(64 + this._gates.filter(gg => gg.type === 'INPUT').length + 1); }
    if (type === 'OUTPUT') { g.label = 'OUT' + (this._gates.filter(gg => gg.type === 'OUTPUT').length + 1 > 1 ? (this._gates.filter(gg => gg.type === 'OUTPUT').length + 1) : ''); }
    if (type === 'CLOCK')  { g.freq = 1; g._phase = 0; }
    this._gates.push(g);
    return g;
  }

  _gateById(id) { return this._gates.find(g => g.id === id) || null; }

  /* ══ Logic propagation (topological sort + eval) ══ */
  _propagate() {
    // Build in-degree map
    const inDeg = {};
    this._gates.forEach(g => { inDeg[g.id] = 0; });
    const deps = {};  // id -> [id of gates this gate depends on]
    this._gates.forEach(g => { deps[g.id] = []; });

    this._wires.forEach(w => {
      inDeg[w.to.gateId]++;
      deps[w.to.gateId].push(w.from.gateId);
    });

    // Kahn's algorithm
    const queue  = this._gates.filter(g => inDeg[g.id] === 0).map(g => g.id);
    const sorted = [];
    const visited = new Set();
    while (queue.length) {
      const id = queue.shift();
      if (visited.has(id)) continue;
      visited.add(id);
      sorted.push(id);
      // find wires going FROM this gate
      this._wires.forEach(w => {
        if (w.from.gateId === id) {
          inDeg[w.to.gateId]--;
          if (inDeg[w.to.gateId] === 0) queue.push(w.to.gateId);
        }
      });
    }
    // any unvisited (loops): add them to sorted anyway
    this._gates.forEach(g => { if (!visited.has(g.id)) sorted.push(g.id); });

    for (const id of sorted) {
      const g   = this._gateById(id);
      if (!g) continue;
      if (g.type === 'INPUT' || g.type === 'CLOCK') continue; // value set externally
      const ins = this._getInputValues(g);
      g.value   = evalGate(g.type, ins);
    }
  }

  _getInputValues(gate) {
    const def = GATE_DEFS[gate.type];
    const vals = new Array(def.ins).fill(0);
    this._wires.forEach(w => {
      if (w.to.gateId !== gate.id) return;
      const src = this._gateById(w.from.gateId);
      if (!src) return;
      const idx = w.to.port === 'in0' ? 0 : 1;
      vals[idx]  = src.value;
    });
    return vals;
  }

  /* ══ Clock ══ */
  _startClock() {
    let last = 0;
    const tick = (now) => {
      this._clockRaf = requestAnimationFrame(tick);
      const dt = (now - last) / 1000;
      const dtMs = now - (last || now);
      last = now;
      let changed = false;
      this._gates.forEach(g => {
        if (g.type !== 'CLOCK') return;
        g._phase = (g._phase || 0) + dt * (g.freq || 1);
        const newVal = g._phase % 1 < 0.5 ? 1 : 0;
        if (newVal !== g.value) { g.value = newVal; changed = true; }
      });
      if (window.LabFX && dtMs > 0) LabFX.particles.update(dtMs);
      if (changed) {
        this._propagate();
        this._updatePanels();
        this.draw();
      }
    };
    this._clockRaf = requestAnimationFrame(tick);
  }

  /* ══ Undo / Redo ══ */
  _pushHistory() {
    const snap = JSON.stringify({ gates: this._gates, wires: this._wires, nextId: this._nextId });
    this._history = this._history.slice(0, this._histIdx + 1);
    this._history.push(snap);
    if (this._history.length > 50) this._history.shift();
    this._histIdx = this._history.length - 1;
  }

  undo() {
    if (this._histIdx <= 0) return;
    this._histIdx--;
    this._restoreHistory(this._history[this._histIdx]);
  }

  redo() {
    if (this._histIdx >= this._history.length - 1) return;
    this._histIdx++;
    this._restoreHistory(this._history[this._histIdx]);
  }

  _restoreHistory(snap) {
    const s = JSON.parse(snap);
    this._gates  = s.gates;
    this._wires  = s.wires;
    this._nextId = s.nextId;
    this._propagate();
    this._updatePanels();
    this.draw();
  }

  /* ══ Fit canvas to element ══ */
  fit() {
    const el = this._canvas.parentElement || this._canvas;
    const dpr = window.devicePixelRatio || 1;
    const w   = el.clientWidth  || 800;
    const h   = el.clientHeight || 500;
    this._canvas.width  = w * dpr;
    this._canvas.height = h * dpr;
    this._canvas.style.width  = w + 'px';
    this._canvas.style.height = h + 'px';
    this._ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    this.draw();
  }

  /* ══ Drawing ══ */
  draw() {
    const ctx  = this._ctx;
    const W    = this._canvas.width  / (window.devicePixelRatio || 1);
    const H    = this._canvas.height / (window.devicePixelRatio || 1);

    ctx.clearRect(0, 0, W, H);

    // grid
    ctx.strokeStyle = 'rgba(255,255,255,0.04)';
    ctx.lineWidth   = 1;
    for (let x = 0; x < W; x += GRID) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
    for (let y = 0; y < H; y += GRID) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }

    // wires
    this._wires.forEach(w => this._drawWire(ctx, w));

    // ghost wire while dragging
    if (this._wireStart) {
      const p = this._wireStart;
      ctx.beginPath();
      ctx.moveTo(p.px, p.py);
      ctx.lineTo(this._mouseX || p.px, this._mouseY || p.py);
      ctx.strokeStyle = 'rgba(255,255,100,0.7)';
      ctx.lineWidth   = 2;
      ctx.setLineDash([4, 4]);
      ctx.stroke();
      ctx.setLineDash([]);
    }

    // gates
    this._gates.forEach(g => this._drawGate(ctx, g));

    if (window.LabFX) LabFX.particles.draw(ctx);
  }

  _drawWire(ctx, w) {
    const g1 = this._gateById(w.from.gateId);
    const g2 = this._gateById(w.to.gateId);
    if (!g1 || !g2) return;
    const p1  = this._portPx(g1, w.from.port);
    const p2  = this._portPx(g2, w.to.port);
    const val = g1.value;
    ctx.beginPath();
    // L-route
    const mx = (p1.x + p2.x) / 2;
    ctx.moveTo(p1.x, p1.y);
    ctx.lineTo(mx, p1.y);
    ctx.lineTo(mx, p2.y);
    ctx.lineTo(p2.x, p2.y);
    ctx.strokeStyle = val ? '#00ff88' : 'rgba(255,255,255,0.25)';
    ctx.lineWidth   = val ? 2.2 : 1.5;
    ctx.stroke();

    // Wire HIGH: animated dot flowing along path
    if (val && window.LabFX) {
      const frac = ((performance.now() * 0.001) % 1);
      // interpolate along L-route: seg1 p1→(mx,p1.y), seg2 (mx,p1.y)→(mx,p2.y), seg3 (mx,p2.y)→p2
      const seg1 = Math.abs(mx - p1.x);
      const seg2 = Math.abs(p2.y - p1.y);
      const seg3 = Math.abs(p2.x - mx);
      const total = seg1 + seg2 + seg3 || 1;
      const dist = frac * total;
      let dx, dy;
      if (dist <= seg1) {
        dx = p1.x + (mx - p1.x) * (dist / (seg1 || 1));
        dy = p1.y;
      } else if (dist <= seg1 + seg2) {
        dx = mx;
        dy = p1.y + (p2.y - p1.y) * ((dist - seg1) / (seg2 || 1));
      } else {
        dx = mx + (p2.x - mx) * ((dist - seg1 - seg2) / (seg3 || 1));
        dy = p2.y;
      }
      ctx.save();
      ctx.beginPath();
      ctx.arc(dx, dy, 3.5, 0, Math.PI * 2);
      ctx.fillStyle = '#06D6E0';
      ctx.shadowColor = '#06D6E0';
      ctx.shadowBlur = 8;
      ctx.fill();
      ctx.restore();
    }
  }

  _drawGate(ctx, g) {
    const def = GATE_DEFS[g.type];
    const hw  = def.w / 2, hh = def.h / 2;
    const x   = g.x, y = g.y;

    // OUTPUT LED glow via LabFX
    const isHigh = g.value === 1;
    if (g.type === 'OUTPUT' && isHigh && window.LabFX) {
      LabFX.glow.drawGlow(ctx, () => {
        ctx.beginPath();
        ctx.roundRect(x - hw, y - hh, def.w, def.h, 6);
        ctx.fill();
      }, { color: '#00FF80', intensity: 18, layers: 2 });
    }

    // gate body
    ctx.beginPath();
    ctx.roundRect(x - hw, y - hh, def.w, def.h, 6);
    let fill = 'rgba(30,30,60,0.9)';
    if (g.type === 'INPUT')  fill = isHigh ? 'rgba(0,220,100,0.35)'  : 'rgba(60,60,100,0.8)';
    if (g.type === 'CLOCK')  fill = isHigh ? 'rgba(0,180,255,0.35)'  : 'rgba(40,40,100,0.8)';
    if (g.type === 'OUTPUT') fill = isHigh ? 'rgba(255,80,80,0.55)'  : 'rgba(60,30,30,0.8)';
    ctx.fillStyle = fill;
    ctx.fill();

    const borderCol = g.type === 'OUTPUT' ? (isHigh ? '#ff6060' : 'rgba(255,255,255,0.2)')
                    : g.type === 'INPUT'  ? (isHigh ? '#00cc66' : 'rgba(255,255,255,0.2)')
                    : g.type === 'CLOCK'  ? (isHigh ? '#00aaff' : 'rgba(255,255,255,0.2)')
                    : 'rgba(155,93,229,0.6)';
    ctx.strokeStyle = borderCol;
    ctx.lineWidth   = 1.5;
    ctx.stroke();

    // label
    ctx.fillStyle   = isHigh ? '#fff' : 'rgba(255,255,255,0.75)';
    ctx.font        = `bold ${def.ins <= 1 ? 10 : 9}px Manrope,sans-serif`;
    ctx.textAlign   = 'center';
    ctx.textBaseline = 'middle';
    const lbl = (g.type === 'INPUT' || g.type === 'OUTPUT') ? g.label : def.label;
    ctx.fillText(lbl, x, y);

    // value badge (INPUT / OUTPUT / CLOCK)
    if (g.type === 'INPUT' || g.type === 'OUTPUT' || g.type === 'CLOCK') {
      ctx.fillStyle   = isHigh ? '#00ff88' : 'rgba(255,255,255,0.3)';
      ctx.font        = 'bold 9px Manrope,sans-serif';
      ctx.fillText(isHigh ? '1' : '0', x + hw - 10, y - hh + 9);
    }

    // ports
    this._drawPorts(ctx, g);
  }

  _drawPorts(ctx, g) {
    const def  = GATE_DEFS[g.type];
    const ports = [];
    if (def.outs > 0) ports.push('out');
    if (def.ins >= 1) ports.push('in0');
    if (def.ins >= 2) ports.push('in1');
    for (const port of ports) {
      const p      = this._portPx(g, port);
      const isOut  = port === 'out';
      const srcGate = isOut ? g : null;
      const val    = isOut ? g.value : this._getInputValues(g)[port === 'in0' ? 0 : 1];
      ctx.beginPath();
      ctx.arc(p.x, p.y, PORT_R, 0, Math.PI * 2);
      ctx.fillStyle   = val ? '#00ff88' : 'rgba(255,255,255,0.3)';
      ctx.fill();
      ctx.strokeStyle = 'rgba(0,0,0,0.5)';
      ctx.lineWidth   = 1;
      ctx.stroke();
    }
  }

  /* ══ Boolean expression panel ══ */
  _updatePanels() {
    this._updateExprPanel();
    this._updateTruthTable();
  }

  _buildExpr(gateId, depth) {
    if (depth > 20) return '…';
    const g = this._gateById(gateId);
    if (!g) return '?';
    if (g.type === 'INPUT') return g.label;
    if (g.type === 'CLOCK') return g.label || 'CLK';

    const srcOf = (port) => {
      const w = this._wires.find(ww => ww.to.gateId === gateId && ww.to.port === port);
      return w ? this._buildExpr(w.from.gateId, depth + 1) : '0';
    };

    const a = g.type !== 'NOT' && g.type !== 'BUFFER' ? srcOf('in0') : srcOf('in0');
    const b = srcOf('in1');

    switch (g.type) {
      case 'AND':    return `(${a} ∧ ${b})`;
      case 'OR':     return `(${a} ∨ ${b})`;
      case 'NOT':    return `¬${a}`;
      case 'XOR':    return `(${a} ⊕ ${b})`;
      case 'NAND':   return `¬(${a} ∧ ${b})`;
      case 'NOR':    return `¬(${a} ∨ ${b})`;
      case 'XNOR':   return `¬(${a} ⊕ ${b})`;
      case 'BUFFER': return a;
      default:       return '?';
    }
  }

  _updateExprPanel() {
    if (!this._exprEl) return;
    const outputs = this._gates.filter(g => g.type === 'OUTPUT');
    if (outputs.length === 0) {
      this._exprEl.textContent = 'Добавьте OUTPUT для вывода выражения';
      return;
    }
    const lines = outputs.map(out => {
      const w = this._wires.find(ww => ww.to.gateId === out.id);
      if (!w) return `${out.label} = ?`;
      const expr = this._buildExpr(w.from.gateId, 0);
      return `${out.label} = ${expr}`;
    });
    this._exprEl.textContent = lines.join('   |   ');
  }

  _updateTruthTable() {
    if (!this._tableEl) return;
    const inputs  = this._gates.filter(g => g.type === 'INPUT');
    const outputs = this._gates.filter(g => g.type === 'OUTPUT');
    if (inputs.length === 0 || outputs.length === 0) {
      this._tableEl.innerHTML = '<span style="color:rgba(255,255,255,0.35)">Добавьте INPUT и OUTPUT</span>';
      return;
    }
    const n = inputs.length;
    if (n > 6) {
      this._tableEl.innerHTML = '<span style="color:rgba(255,255,255,0.35)">Слишком много входов (макс 6)</span>';
      return;
    }
    const rows = 1 << n;
    // save current values
    const savedVals = inputs.map(g => g.value);

    let html = '<table class="logic-tt"><thead><tr>';
    inputs.forEach(g  => { html += `<th>${g.label}</th>`; });
    outputs.forEach(g => { html += `<th>${g.label}</th>`; });
    html += '</tr></thead><tbody>';

    // determine current row
    const curRow = savedVals.reduce((acc, v, i) => acc | (v << (n - 1 - i)), 0);

    for (let r = 0; r < rows; r++) {
      inputs.forEach((g, i) => { g.value = (r >> (n - 1 - i)) & 1; });
      this._propagate();
      const isCur = r === curRow;
      html += `<tr${isCur ? ' class="logic-tt-cur"' : ''}>`;
      inputs.forEach((g, i) => { html += `<td>${(r >> (n - 1 - i)) & 1}</td>`; });
      outputs.forEach(g => { html += `<td style="color:${g.value ? '#00ff88' : 'rgba(255,255,255,0.4)'}">${g.value}</td>`; });
      html += '</tr>';
    }
    html += '</tbody></table>';
    this._tableEl.innerHTML = html;

    // restore
    inputs.forEach((g, i) => { g.value = savedVals[i]; });
    this._propagate();
  }

  /* ══ Presets ══ */
  preset(name) {
    this._pushHistory();
    this._gates = [];
    this._wires = [];
    this._nextId = 1;
    if (window.LabFX) LabFX.sound.play('chime', { pitch: 1.0, volume: 0.3 });
    const add  = (type, x, y) => this._addGate(type, x, y);
    const wire = (a, ap, b, bp) => this._wires.push({ from: { gateId: a.id, port: ap }, to: { gateId: b.id, port: bp } });

    switch (name) {
      case 'half-adder': {
        const A   = add('INPUT',  80, 120); A.label  = 'A';
        const B   = add('INPUT',  80, 200); B.label  = 'B';
        const xor = add('XOR',   200, 120);
        const and = add('AND',   200, 200);
        const S   = add('OUTPUT', 320, 120); S.label  = 'S';
        const C   = add('OUTPUT', 320, 200); C.label  = 'C';
        wire(A, 'out', xor, 'in0'); wire(B, 'out', xor, 'in1');
        wire(A, 'out', and, 'in0'); wire(B, 'out', and, 'in1');
        wire(xor, 'out', S, 'in0');
        wire(and, 'out', C, 'in0');
        break;
      }
      case 'full-adder': {
        const A    = add('INPUT',   60, 100); A.label   = 'A';
        const B    = add('INPUT',   60, 180); B.label   = 'B';
        const Cin  = add('INPUT',   60, 260); Cin.label = 'Cin';
        const xr1  = add('XOR',    180, 140);
        const xr2  = add('XOR',    300, 140);
        const an1  = add('AND',    180, 220);
        const an2  = add('AND',    300, 220);
        const or1  = add('OR',     400, 220);
        const S    = add('OUTPUT', 420, 140); S.label   = 'S';
        const Cout = add('OUTPUT', 520, 220); Cout.label = 'Cout';
        wire(A, 'out', xr1, 'in0');   wire(B, 'out', xr1, 'in1');
        wire(xr1, 'out', xr2, 'in0'); wire(Cin, 'out', xr2, 'in1');
        wire(A, 'out', an1, 'in0');   wire(B, 'out', an1, 'in1');
        wire(xr1, 'out', an2, 'in0'); wire(Cin, 'out', an2, 'in1');
        wire(an1, 'out', or1, 'in0'); wire(an2, 'out', or1, 'in1');
        wire(xr2, 'out', S, 'in0');
        wire(or1, 'out', Cout, 'in0');
        break;
      }
      case 'rs-latch': {
        // Cross-coupled NOR gates: Q=NOR(R,Qbar), Qbar=NOR(S,Q)
        // We simplify: R→NOR1, S→NOR2 cross-coupled; initial state stabilised
        const R   = add('INPUT',  80, 120); R.label  = 'R';
        const S   = add('INPUT',  80, 220); S.label  = 'S';
        const nr1 = add('NOR',   220, 120);
        const nr2 = add('NOR',   220, 220);
        const Q   = add('OUTPUT', 340, 120); Q.label  = 'Q';
        const Qb  = add('OUTPUT', 340, 220); Qb.label = 'Q̅';
        wire(R, 'out', nr1, 'in0');
        wire(S, 'out', nr2, 'in1');
        // Cross connections — we add them and propagate will stabilise
        wire(nr2, 'out', nr1, 'in1');
        wire(nr1, 'out', nr2, 'in0');
        wire(nr1, 'out', Q, 'in0');
        wire(nr2, 'out', Qb, 'in0');
        // run propagation twice to stabilise
        this._propagate(); this._propagate();
        break;
      }
      case 'd-latch': {
        // D latch: Q = D when CLK=1, holds otherwise
        // SR from D: S=D∧CLK, R=¬D∧CLK
        const D    = add('INPUT',  60, 140); D.label   = 'D';
        const CLK  = add('CLOCK', 60, 220); CLK.label = 'CLK'; CLK.freq = 1;
        const notD = add('NOT',   160, 180);
        const an1  = add('AND',   260, 120);
        const an2  = add('AND',   260, 220);
        const nr1  = add('NOR',   360, 120);
        const nr2  = add('NOR',   360, 220);
        const Q    = add('OUTPUT', 480, 120); Q.label  = 'Q';
        const Qb   = add('OUTPUT', 480, 220); Qb.label = 'Q̅';
        wire(D, 'out', notD, 'in0');
        wire(D, 'out', an1, 'in0');    wire(CLK, 'out', an1, 'in1');
        wire(notD, 'out', an2, 'in0'); wire(CLK, 'out', an2, 'in1');
        wire(an1, 'out', nr1, 'in0');  wire(nr2, 'out', nr1, 'in1');
        wire(an2, 'out', nr2, 'in1');  wire(nr1, 'out', nr2, 'in0');
        wire(nr1, 'out', Q, 'in0');
        wire(nr2, 'out', Qb, 'in0');
        break;
      }
      case 'decoder-2to4': {
        const A   = add('INPUT',  60, 100); A.label = 'A';
        const B   = add('INPUT',  60, 200); B.label = 'B';
        const nA  = add('NOT',   160, 100);
        const nB  = add('NOT',   160, 200);
        const g0  = add('AND',   280,  80);
        const g1  = add('AND',   280, 160);
        const g2  = add('AND',   280, 240);
        const g3  = add('AND',   280, 320);
        const o0  = add('OUTPUT', 400,  80); o0.label = 'Y0';
        const o1  = add('OUTPUT', 400, 160); o1.label = 'Y1';
        const o2  = add('OUTPUT', 400, 240); o2.label = 'Y2';
        const o3  = add('OUTPUT', 400, 320); o3.label = 'Y3';
        wire(A, 'out', nA, 'in0');
        wire(B, 'out', nB, 'in0');
        wire(nA, 'out', g0, 'in0'); wire(nB, 'out', g0, 'in1');
        wire(A, 'out',  g1, 'in0'); wire(nB, 'out', g1, 'in1');
        wire(nA, 'out', g2, 'in0'); wire(B, 'out',  g2, 'in1');
        wire(A, 'out',  g3, 'in0'); wire(B, 'out',  g3, 'in1');
        wire(g0, 'out', o0, 'in0');
        wire(g1, 'out', o1, 'in0');
        wire(g2, 'out', o2, 'in0');
        wire(g3, 'out', o3, 'in0');
        break;
      }
      case 'mux-2to1': {
        // Y = (A ∧ ¬S) ∨ (B ∧ S)
        const A  = add('INPUT',  60, 100); A.label = 'A';
        const B  = add('INPUT',  60, 200); B.label = 'B';
        const Sel= add('INPUT',  60, 300); Sel.label = 'S';
        const nS = add('NOT',   160, 300);
        const an1= add('AND',   280, 120);
        const an2= add('AND',   280, 240);
        const or1= add('OR',    380, 180);
        const Y  = add('OUTPUT', 480, 180); Y.label = 'Y';
        wire(Sel, 'out', nS, 'in0');
        wire(A, 'out', an1, 'in0');   wire(nS, 'out', an1, 'in1');
        wire(B, 'out', an2, 'in0');   wire(Sel, 'out', an2, 'in1');
        wire(an1, 'out', or1, 'in0'); wire(an2, 'out', or1, 'in1');
        wire(or1, 'out', Y, 'in0');
        break;
      }
      default: {
        const A = add('INPUT',  100, 160); A.label = 'A';
        const B = add('INPUT',  100, 240); B.label = 'B';
        const g = add('AND',    240, 200);
        const O = add('OUTPUT', 360, 200);
        wire(A, 'out', g, 'in0'); wire(B, 'out', g, 'in1');
        wire(g, 'out', O, 'in0');
      }
    }
    this._propagate();
    this._updatePanels();
    this.draw();
  }

  /* ── Clear ── */
  clear() {
    this._pushHistory();
    this._gates = [];
    this._wires = [];
    this._propagate();
    this._updatePanels();
    this.draw();
  }

  /* ── Continuous draw loop for wire animations ── */
  _startDrawLoop() {
    const loop = () => {
      this._raf = requestAnimationFrame(loop);
      // Only redraw if any wire is HIGH (animated dot)
      const anyHigh = this._wires.some(w => {
        const g = this._gateById(w.from.gateId);
        return g && g.value === 1;
      });
      if (anyHigh) this.draw();
    };
    this._raf = requestAnimationFrame(loop);
  }

  /* ── Destroy ── */
  destroy() {
    if (this._clockRaf) cancelAnimationFrame(this._clockRaf);
    if (this._raf)      cancelAnimationFrame(this._raf);
  }

  /* ── Set tool ── */
  setTool(t) { this._tool = t; }
}

/* ═══════════════════════════════════════════════════════════
   Global helpers called from HTML
═══════════════════════════════════════════════════════════ */
var logicSim = null;
var _logicTableOpen = true;

function logicTool(t, el) {
  if (logicSim) logicSim.setTool(t);
  document.querySelectorAll('.lgc-tool-btn').forEach(b => b.classList.toggle('active', b.dataset.tool === t));
}

function logicPreset(name) {
  if (logicSim) logicSim.preset(name);
}

function logicClearAll() {
  if (logicSim) logicSim.clear();
}

function logicToggleTable() {
  _logicTableOpen = !_logicTableOpen;
  const panel = document.getElementById('logic-tt-panel');
  if (panel) panel.style.display = _logicTableOpen ? '' : 'none';
  const btn = document.getElementById('btn-logic-tt');
  if (btn) btn.classList.toggle('active', _logicTableOpen);
}

function _openLogic() {
  document.getElementById('sim-topbar-title').textContent = 'Логические схемы';
  _simShow('sim-logic');
  requestAnimationFrame(() => requestAnimationFrame(() => {
    const canvas  = document.getElementById('logic-canvas');
    const exprEl  = document.getElementById('logic-expr');
    const tableEl = document.getElementById('logic-tt-body');
    if (!logicSim) {
      logicSim = new LogicSim(canvas, exprEl, tableEl);
    } else {
      // re-attach panels in case DOM was re-created
      logicSim._exprEl  = exprEl;
      logicSim._tableEl = tableEl;
    }
    logicSim.fit();
    if (logicSim._gates.length === 0) logicSim.preset('half-adder');
    logicSim._updatePanels();
    logicSim.draw();
    // select tool active by default
    logicTool('select', null);
  }));
}
