// viewer_core.js — the ONE symbol-viewer engine, compiled into BOTH the live
// adom-symbol app (SymView) AND the standalone embed export. Neither file should
// reimplement pan/zoom or pin-hover; they call into here so they can never drift.
window.AdomSymViewer = (function(){
  var ZOOM_K = 0.0022;            // wheel sensitivity (lower = gentler). The feel.
  var MIN_SCALE = 0.05, MAX_SCALE = 60;

  function clampScale(s){ return Math.max(MIN_SCALE, Math.min(s, MAX_SCALE)); }

  // Device-independent wheel zoom: normalize deltaMode + cap one notch, then hand
  // the pointer-anchored factor to the host's zoomAt(cx, cy, factor). This is the
  // exact math the app has always used — the embed now shares it verbatim.
  function attachWheelZoom(el, zoomAt){
    el.addEventListener('wheel', function(e){
      e.preventDefault();
      var d = e.deltaY;
      if(e.deltaMode === 1) d *= 16;                  // lines  -> px
      else if(e.deltaMode === 2) d *= el.clientHeight; // pages -> px
      d = Math.max(-50, Math.min(50, d));              // cap one event's contribution
      var r = el.getBoundingClientRect();
      zoomAt(e.clientX - r.left, e.clientY - r.top, Math.exp(-d * ZOOM_K));
    }, { passive: false });
  }

  // ---- shared pin-hover engine (glyph pairing + hit-box geometry) ----
  function norm(s){ return String(s || '').replace(/[~{}]/g, '').trim(); }

  // Pair each invisible <text opacity="0"> with the visible <g class="stroked-text">
  // glyph that follows it in document order (handles rotated top/bottom pins).
  function glyphLabels(svg){
    var out = [], pending = null;
    if(!svg) return out;
    svg.querySelectorAll('text, g.stroked-text').forEach(function(n){
      if(n.classList && n.classList.contains('stroked-text')){
        if(pending){ var k = norm(pending.textContent); if(k) out.push({ k: k, group: n }); pending = null; }
      } else if(n.getAttribute && n.getAttribute('opacity') === '0'){ pending = n; }
    });
    return out;
  }

  // Build one transparent hit-rect per pin label (number + name), matched to each
  // pin's glyph by proximity to the pin tip (so duplicate names resolve to the
  // right pin). Rects are appended to `svg` in user space so they pan/zoom with it.
  //   pins:   [{number,name,x,y,angle,length,...}]
  //   offset: {x,y} symbol->svg-unit origin (state.offset)
  //   NS:     the svg namespace
  //   cb:     { enter(pin,ev), move(ev), leave(), click(pin,ev)? }
  // Returns { textMap } so the host can highlight a pin's glyphs on demand.
  function buildPinHits(svg, pins, offset, NS, cb){
    var textMap = new Map();
    if(!svg || !offset || !pins || !pins.length) return { textMap: textMap };
    cb = cb || {};
    var gl = glyphLabels(svg);
    gl.forEach(function(o){ if(!textMap.has(o.k)) textMap.set(o.k, []); textMap.get(o.k).push(o.group); });
    var labels = [];
    gl.forEach(function(o){ var b; try { b = o.group.getBBox(); } catch(e){ return; }
      if(!b || !b.width) return;
      labels.push({ k: o.k, box: b, cx: b.x + b.width/2, cy: b.y + b.height/2, used: false }); });
    function pick(k, ax, ay){ k = norm(k); var best = null, bd = 1e9;
      labels.forEach(function(l){ if(l.used || l.k !== k) return; var d = Math.hypot(l.cx - ax, l.cy - ay); if(d < bd){ bd = d; best = l; } });
      if(best) best.used = true; return best; }
    function addBox(b, p){
      var pad = 0.45, r = document.createElementNS(NS, 'rect');
      r.setAttribute('class', 'hit');
      r.setAttribute('x', b.x - pad); r.setAttribute('y', b.y - pad);
      r.setAttribute('width', b.width + pad*2); r.setAttribute('height', b.height + pad*2); r.setAttribute('rx', .3);
      r.addEventListener('mouseenter', function(ev){ if(cb.enter) cb.enter(p, ev); });
      r.addEventListener('mousemove', function(ev){ if(cb.move) cb.move(ev); });
      r.addEventListener('mouseleave', function(){ if(cb.leave) cb.leave(); });
      if(cb.click) r.addEventListener('click', function(ev){ cb.click(p, ev); });
      svg.appendChild(r);
    }
    pins.forEach(function(p){
      var tipX = offset.x + p.x, tipY = offset.y - p.y, L = p.length || 2.54, dx = 0, dy = 0;
      if(p.angle === 0) dx = 1; else if(p.angle === 180) dx = -1; else if(p.angle === 90) dy = -1; else if(p.angle === 270) dy = 1;
      var nl = pick(p.number, tipX + dx*L*0.5, tipY + dy*L*0.5 - (dx !== 0 ? 0.9 : 0));  // number: over the wire
      var ml = pick(p.name,   tipX + dx*(L+2), tipY + dy*(L+2));                          // name: inside the body
      if(nl) addBox(nl.box, p);
      if(ml) addBox(ml.box, p);
      if(!nl && !ml){                                                                      // discrete parts: no glyphs
        var ex = tipX + dx*L, ey = tipY + dy*L, HT = 1.0;
        addBox({ x: Math.min(tipX, ex) - (dx === 0 ? HT : 0.3), y: Math.min(tipY, ey) - (dy === 0 ? HT : 0.3),
                 width: Math.abs(ex - tipX) + (dx === 0 ? HT*2 : 0.6), height: Math.abs(ey - tipY) + (dy === 0 ? HT*2 : 0.6) }, p);
      }
    });
    return { textMap: textMap };
  }

  return {
    ZOOM_K: ZOOM_K, clampScale: clampScale, attachWheelZoom: attachWheelZoom,
    norm: norm, glyphLabels: glyphLabels, buildPinHits: buildPinHits
  };
})();