// scripting_dom.js — PRODUCTION DOM shim for .jsx-on-JavaScriptCore (promoted from
// scripting_poc/dom_shim.js). The minimal browser-DOM surface ai-dom.js/ai-runner.js
// actually touch (exhaustive census in Reference/jsx-jsc-feasibility.md §1), backed by the
// LIVE svgscene::Scene through the `__scene` C entry points wired in scripting.mm. NO
// WebKit, NO layout engine, NO canvas — pure structural DOM + one getBBox delegated to the
// core (geometry.cpp for shapes/paths, text_layout::measure for <text>).
//
// DIFFERENCE FROM THE POC SHIM (the only two):
//   1. A minimal `global.svgCanvas` exposing getNextId → __scene.nextId(): so ai-dom's
//      AI.nextId() returns the getNextId-faithful "svg_N" (IdAllocator on the native side),
//      NOT the 'aidom_N' fallback. addSvgElementFromJson is intentionally ABSENT, so
//      AI.create still takes its pure-DOM createElementNS path (which this shim backs on
//      the Scene through a Transaction) — we want the id parity, not the svgCanvas element
//      factory. Everything else ai-dom reaches on svgCanvas (getResolution, undoMgr, layers)
//      is absent → its try/catch fallbacks fire, exactly as censused in the feasibility doc.
//   2. Mutations are transactional on the native side (see scripting.mm) — the shim is
//      unchanged for that; it just calls the same __scene.setAttr/append/… blocks, which
//      now record svgscene::Change entries into the open "Run script" transaction.
//   3. (SVGE-1158) Deleting keeps the selection honest: `removeChild` prunes the removed node
//      from the run's selection and `svgCanvas.removeFromSelection` exists (ai-dom's
//      `item.selected = false` calls it). Both are engine-neutral bookkeeping — no Change.
//   4. (SVGE-1159) `svgCanvas.rasterPixels` — the READ-ONLY window onto a placed bitmap's
//      decoded texels (raster_ops.h), the native backing of ai-dom's RasterItem. It exists
//      ONLY on this engine; the webview twin has no native scene, so ai-dom degrades to a
//      clear error there rather than to wrong colours.
//
// CENSUS OF RECORD (unchanged from the POC — the API ai-dom.js + ai-runner.js use):
//   document.getElementById · document.createElementNS
//   getAttribute setAttribute removeAttribute hasAttribute
//   appendChild insertBefore removeChild cloneNode
//   querySelector(':scope > title')  querySelectorAll(<csv-of-tag-names>)
//   parentNode childNodes children firstChild nextSibling
//   nextElementSibling previousElementSibling tagName textContent style.mixBlendMode
//   getBBox  — the ONLY browser-LAYOUT dependency (ai-dom.js:351)

(function (global) {
  'use strict';
  var S = global.__scene;                 // native scene-graph entry points (C blocks)
  var SVGNS = 'http://www.w3.org/2000/svg';

  // --- one stable JS wrapper per node id (identity: ai-dom keys a WeakMap on these,
  //     and does `el.parentNode !== host` / `n === this._el` identity comparisons) ----
  var REG = Object.create(null);
  function el(id) {
    id = +id;
    if (!id) return null;
    if (REG[id]) return REG[id];
    var w = new Element(id);
    REG[id] = w;
    return w;
  }

  function Element(id) { this.__id = id; this.nodeType = 1; }

  Element.prototype.getAttribute = function (name) {
    var v = S.getAttr(this.__id, name);
    return (v === undefined || v === null) ? null : v;
  };
  Element.prototype.setAttribute = function (name, value) {
    S.setAttr(this.__id, name, String(value));
  };
  Element.prototype.removeAttribute = function (name) { S.removeAttr(this.__id, name); };
  Element.prototype.hasAttribute = function (name) { return S.getAttr(this.__id, name) != null; };

  Element.prototype.appendChild = function (child) {
    S.append(this.__id, child.__id); return child;
  };
  Element.prototype.insertBefore = function (child, ref) {
    S.insertBefore(this.__id, child.__id, ref ? ref.__id : 0); return child;
  };
  // removeChild also drops the node from the run's selection (SVGE-1158). A deleted object
  // cannot stay selected: ai-dom's PageItem.remove() is `parentNode.removeChild(el)`, so
  // without this `doc.selection` kept handing back wrappers for nodes that no longer exist
  // and any later read on them (getBBox/getAttribute) hit a dead id. Pruning here — rather
  // than in one caller — keeps EVERY delete path honest (item.remove(), Layer.remove(),
  // svgCanvas.clear(), a script's own removeChild).
  Element.prototype.removeChild = function (child) {
    S.remove(this.__id, child.__id);
    if (SEL) { var k = SEL.indexOf(child); if (k >= 0) SEL.splice(k, 1); }
    return child;
  };
  Element.prototype.cloneNode = function (deep) { return el(S.clone(this.__id, !!deep)); };

  // querySelector: the ONLY pattern ai-dom passes is ':scope > title' (direct child).
  Element.prototype.querySelector = function (sel) {
    if (sel === ':scope > title') {
      var kids = S.children(this.__id);
      for (var i = 0; i < kids.length; i++) if (S.tag(kids[i]) === 'title') return el(kids[i]);
      return null;
    }
    var all = this.querySelectorAll(sel);
    return all.length ? all[0] : null;
  };
  // querySelectorAll: sel is a comma-separated TAG-NAME list (GEOM_TAGS.join(','), 'g',
  // 'text', …) — deep descendants matching any of those tags. Not general CSS.
  Element.prototype.querySelectorAll = function (sel) {
    return S.descendants(this.__id, sel).map(el);
  };

  Object.defineProperty(Element.prototype, 'tagName', { get: function () { return S.tag(this.__id); } });
  Object.defineProperty(Element.prototype, 'parentNode', { get: function () { return el(S.parent(this.__id)); } });
  Object.defineProperty(Element.prototype, 'childNodes', { get: function () { return S.children(this.__id).map(el); } });
  Object.defineProperty(Element.prototype, 'children', { get: function () { return S.children(this.__id).map(el); } });
  Object.defineProperty(Element.prototype, 'firstChild', { get: function () { return el(S.firstChild(this.__id)); } });
  Object.defineProperty(Element.prototype, 'nextSibling', { get: function () { return el(S.nextSibling(this.__id)); } });
  Object.defineProperty(Element.prototype, 'nextElementSibling', { get: function () { return el(S.nextSibling(this.__id)); } });
  Object.defineProperty(Element.prototype, 'previousElementSibling', { get: function () { return el(S.prevSibling(this.__id)); } });
  Object.defineProperty(Element.prototype, 'textContent', {
    get: function () { return S.getText(this.__id); },
    set: function (v) { S.setText(this.__id, v == null ? '' : String(v)); }
  });
  // style: only .mixBlendMode is read/written (blendingMode). Backed by a `style` attr so
  // it survives serialization; no CSS cascade needed (SVG presentation attribute).
  Object.defineProperty(Element.prototype, 'style', {
    get: function () {
      var self = this;
      if (this.__style) return this.__style;
      this.__style = {
        get mixBlendMode() {
          var m = /mix-blend-mode:\s*([^;]+)/.exec(self.getAttribute('style') || '');
          return m ? m[1].trim() : '';
        },
        set mixBlendMode(v) { self.setAttribute('style', 'mix-blend-mode:' + v); }
      };
      return this.__style;
    }
  });

  // getBBox — the single browser-layout call. Delegated to the core: geometry.cpp exact
  // bbox for shapes/paths, union for groups, and text_layout::measure() for <text>
  // (baseline/ascent/advance → the box WebKit reports; see scripting.mm bboxOf).
  Element.prototype.getBBox = function () {
    var b = S.bbox(this.__id);        // [x, y, width, height]
    return { x: b[0], y: b[1], width: b[2], height: b[3] };
  };

  // --- document ---------------------------------------------------------------------
  var document = {
    getElementById: function (id) { var n = S.getElementById(id); return n ? el(n) : null; },
    createElementNS: function (ns, tag) { return el(S.create(tag)); },
    createElement: function (tag) { return el(S.create(tag)); }
  };

  global.document = document;
  global.SVGElement = Element;

  var GEOM_SEL = 'rect,circle,ellipse,line,polyline,polygon,path,text,image,use';

  // --- MUTABLE selection state (SVGE-1121) --------------------------------------------
  // The native `__scene.selection()` is the run's INITIAL selection (read-only, fixed at
  // run start). ai-dom's `doc.selection` getter and existing selection-reading .jsx only
  // READ it, so seeding a JS-side array from it once and returning that preserves their
  // behaviour byte-for-byte. But the #636 SVGETest oracle also WRITES the selection
  // (createRect→selectOnly, selectAll, group, …) and then asserts on it in the SAME run —
  // so the selection must be mutable JS-side. SEL holds shim Element wrappers (stable
  // identity via el()'s REG cache, so wrapItem's WeakMap / `n === this._el` checks hold).
  var SEL = null;
  function selSeed() {
    if (SEL) return SEL;
    SEL = [];
    var ids = (S.selection ? S.selection() : null) || [];
    for (var i = 0; i < ids.length; i++) { var w = el(ids[i]); if (w) SEL.push(w); }
    return SEL;
  }

  // Minimal svgCanvas backing (SVGE-1024 gradient route + SVGE-1121 test oracle). getNextId
  // (fork-faithful "svg_N" via the native IdAllocator) is the sole reason ai-dom gets real
  // getNextId ids instead of 'aidom_N'; getSelectedElems feeds ai-dom's `doc.selection`
  // getter (Document.prototype.selection → AI.canvas().getSelectedElems().filter(Boolean) →
  // wrapAll(…, wrapItem)). addSvgElementFromJson stays deliberately absent so AI.create takes
  // its createElementNS path (backed here). The extra surface below (clear/setResolution/
  // getSvgString/select*/group/zorder/undoMgr) exists ONLY so the #636 SVGETest oracle works
  // in this webview-free (Skia master) context; it is implemented over the same __scene
  // primitives (plus __scene.serialize), all inside the open "Run script" transaction.
  global.svgCanvas = {
    getNextId: function () { return S.nextId(); },
    // svgfeedback SVGE-1439 — the ACTIVE LAYER. ai-dom's AI.currentLayerGroup() asks for
    // `getCurrentDrawing().getCurrentLayer()`; the shim had no such surface, so the call threw
    // and ai-dom fell all the way back to #svgcontent. Art a script created without naming a
    // container therefore landed OUTSIDE every layer — invisible in the Layers panel and, with
    // the raw/element index mismatch fixed alongside, still not where Illustrator puts it.
    // `__scene.currentLayer()` answers the layer the Layers panel points at (top-most when the
    // pointer is unset), or 0 in a document with no layer — where returning null keeps ai-dom's
    // #svgcontent fallback, exactly as before.
    getCurrentDrawing: function () {
      return { getCurrentLayer: function () { return S.currentLayer ? el(S.currentLayer()) : null; } };
    },
    getResolution: function () {
      var r = document.getElementById('svgcontent');
      return { w: r ? (parseFloat(r.getAttribute('width')) || 0) : 0,
               h: r ? (parseFloat(r.getAttribute('height')) || 0) : 0 };
    },
    // -- selection (mutable; see SEL above) --
    getSelectedElems: function () { return selSeed().slice(); },
    selectOnly: function (arr) { SEL = (arr || []).filter(Boolean).slice(); },
    addToSelection: function (arr) {
      selSeed();
      for (var i = 0; i < (arr || []).length; i++) { var e = arr[i]; if (e && SEL.indexOf(e) < 0) SEL.push(e); }
    },
    // ai-dom's PageItem `selected = false` setter calls this (it is guarded by try/catch, so
    // its absence used to make deselection a silent no-op). SVGE-1158 — the counterpart of
    // addToSelection, and the explicit form of the pruning removeChild does implicitly.
    removeFromSelection: function (arr) {
      selSeed();
      for (var i = 0; i < (arr || []).length; i++) { var k = SEL.indexOf(arr[i]); if (k >= 0) SEL.splice(k, 1); }
    },
    clearSelection: function () { SEL = []; },
    selectAllInCurrentLayer: function () {
      var r = document.getElementById('svgcontent');
      SEL = r ? r.querySelectorAll(GEOM_SEL) : [];
    },
    call: function () {},   // ai-dom's AI.commit() fires cv.call('changed', …): a no-op here.
    // -- document reset / serialization (SVGETest.reset / getSVGString) --
    clear: function () {
      var r = document.getElementById('svgcontent');
      if (r) { var kids = r.children; for (var i = kids.length - 1; i >= 0; i--) r.removeChild(kids[i]); }
      SEL = [];
    },
    setResolution: function (w, h) {
      var r = document.getElementById('svgcontent');
      if (r) { r.setAttribute('width', w); r.setAttribute('height', h); }
      return true;
    },
    setBackground: function () {},   // SVG has no document background attr — honest no-op.
    getSvgString: function () { return S.serialize ? S.serialize() : ''; },
    // -- structure (group/ungroup/z-order), pure DOM reparents over the shim --
    groupSelectedElements: function () {
      var root = document.getElementById('svgcontent');
      var els = selSeed().slice();
      if (!root || !els.length) return;
      var g = document.createElementNS(SVGNS, 'g');
      g.setAttribute('id', S.nextId ? S.nextId() : ('g' + els.length));
      root.appendChild(g);
      for (var i = 0; i < els.length; i++) g.appendChild(els[i]);
      SEL = [g];
    },
    ungroupSelectedElement: function () {
      var out = [], groups = selSeed().slice();
      for (var i = 0; i < groups.length; i++) {
        var g = groups[i];
        if (!g || g.tagName !== 'g') { if (g) out.push(g); continue; }
        var parent = g.parentNode; if (!parent) continue;
        var kids = g.children;
        for (var j = 0; j < kids.length; j++) { parent.insertBefore(kids[j], g); out.push(kids[j]); }
        parent.removeChild(g);
      }
      SEL = out;
    },
    moveToTopSelectedElement: function () {
      var els = selSeed();
      for (var i = 0; i < els.length; i++) { var p = els[i].parentNode; if (p) p.appendChild(els[i]); }
    },
    moveToBottomSelectedElement: function () {
      var els = selSeed();
      for (var i = 0; i < els.length; i++) { var p = els[i].parentNode; if (p) p.insertBefore(els[i], p.firstChild); }
    },
    moveUpDownSelected: function (dir) {
      var els = selSeed();
      for (var i = 0; i < els.length; i++) {
        var e = els[i], p = e.parentNode; if (!p) continue;
        if (dir === 'Up') { var nx = e.nextElementSibling; if (nx) p.insertBefore(e, nx.nextElementSibling); }
        else { var pv = e.previousElementSibling; if (pv) p.insertBefore(e, pv); }
      }
    },
    // -- undo manager stub. Native runs commit ONE "Run script" tx per do-script, so the
    //    per-op undo stack the webview build exposes does not exist here (documented
    //    native-oracle limit, matching the spec's "scripted create* not undoable" note).
    //    Stubbed so SVGETest.undo/redo/nextUndoLabel return honest zero-step results
    //    instead of throwing, and reset()/loadFixture()'s resetUndoStack() is a no-op.
    undoMgr: {
      resetUndoStack: function () {},
      getUndoStackSize: function () { return 0; },
      getRedoStackSize: function () { return 0; },
      getNextUndoCommandText: function () { return ''; },
      getNextRedoCommandText: function () { return ''; },
      undo: function () {},
      redo: function () {},
      addCommandToHistory: function () {}
    },
    // svgfeedback SVGE-1024 — native gradient route: authors (or dedup-reuses)
    // a gradient def inside the open "Run script" tx and returns its id (or
    // null). ai-dom's applyGradient uses it when the webview main bridge is
    // absent, then sets fill/stroke='url(#id)' via the normal attribute path.
    authorGradientDef: (S.gradientDef ? function (specJson) {
      var r = S.gradientDef(specJson);
      return (r === undefined || r === null) ? null : r;
    } : undefined),

    // svgfeedback SVGE-1159 — PIXEL COLOURS of placed bitmaps (paper.js "Using Pixel
    // Colors", ported to the Skia master). ai-dom's RasterItem reaches the decoded
    // texels of an <image> ONLY through this namespace, so the DOM stays engine-neutral:
    // present here (JSC/Skia) ⇒ the API works; absent (the WKWebView twin, which has no
    // native scene) ⇒ RasterItem throws a clear "requires the Skia engine" error instead
    // of silently returning wrong colours. Every call is a PURE READ — nothing enters the
    // open "Run script" transaction, so a pixel-reading script pushes no undo step.
    //   size(el)              → [width, height]  intrinsic bitmap pixels (null if undecodable)
    //   pixel(el, x, y)       → [r, g, b, a]     r/g/b 0..255 unpremultiplied, a 0..1
    //                                            (x,y in the bitmap's own pixel grid)
    //   matrix(el)            → [docToPixel a..f, pixelToDoc a..f] — folds the SVG placement
    //                                            (x/y/width/height + preserveAspectRatio) and
    //                                            every ancestor transform
    //   average(el, specJson) → [r, g, b, a] | null, area in DOCUMENT coords as
    //                                            {"rect":[x,y,w,h]} or {"node":<id>}
    // svgfeedback SVGE-1250 — VARIABLE-FONT AXES for scripts. `svgCanvas.fontAxes(family)`
    // answers what a family can VARY: an array of { tag, min, def, max, hidden } in the font's
    // own fvar order, or [] for a static font / an unknown family. It is the same probe the
    // Character palette's sliders use (native TextEngine → the resolved typeface's fvar), so a
    // script can discover an axis and then APPLY it with the attribute writer it already has —
    // item.setAttr('font-variation-settings', '"wght" 650') — which part A already made render,
    // measure and persist. A PURE READ: nothing enters the "Run script" transaction, so probing
    // fonts pushes no undo step. Absent (undefined) on the WKWebView twin, which has no native
    // font engine — a script can feature-test it exactly like rasterPixels.
    fontAxes: (S.fontAxes ? function (family) {
      var out = S.fontAxes(String(family || ''));
      return out || [];
    } : undefined),

    // svgfeedback SVGE-1315 — INSTALLED FONTS for scripts. `svgCanvas.fontList()` answers
    // every installed FACE as { name (PostScript), family, style }, i.e. the enumeration
    // ai-dom's `app.textFonts` is built on. Same CoreText set the Character palette's family
    // popup shows, so a script can only pick something the renderer really resolves. PURE
    // READ — no Change, no undo step. Absent (undefined) on the WKWebView twin, where ai-dom
    // falls back to the editor's own font popup: a script can feature-test it like fontAxes.
    fontList: (S.fontList ? function () { return S.fontList() || []; } : undefined),

    // svgfeedback SVGE-1315 — INLINE TEXT RUNS, the read/write pair per-character styling
    // needs. `host` is a <text> or ONE line <tspan>; the runs are its inline content in
    // document order as [{text, style, fill}]. The shim's childNodes is elements-only, so
    // the interleaving of bare text and run <tspan>s is only observable through this native
    // walk — reconstructing it in JS would silently drop the unstyled fragments. Writing
    // goes through the open "Run script" transaction like every other scripted edit.
    textRuns: (S.textRuns ? {
      read: function (host) { return host ? (S.textRuns(host.__id) || []) : []; },
      write: function (host, runs) { if (host) S.setTextRuns(host.__id, JSON.stringify(runs || [])); }
    } : undefined),

    rasterPixels: (S.rasterSize ? {
      size: function (el) { return el ? S.rasterSize(el.__id) : null; },
      pixel: function (el, x, y) { return el ? S.rasterPixel(el.__id, x, y) : null; },
      matrix: function (el) { return el ? S.rasterMatrix(el.__id) : null; },
      average: function (el, specJson) { return el ? S.rasterAverage(el.__id, specJson) : null; },
      // The shim's numeric handle for an element — ai-dom needs it to name the AREA node in
      // an average() spec ({"node": <id>}) without knowing anything about the id space.
      handle: function (el) { return el ? el.__id : 0; }
    } : undefined)
  };

  // Minimal `global.svgedit` — svgfeedback SVGE-1121 (rework). The #636 SVGETest oracle
  // (test-hooks.js, loaded UNMODIFIED into this runner) computes a TRANSFORM-AWARE bbox via
  // svgedit.utilities.getBBox + svgedit.math.getMatrix/transformBox, exactly like the webview.
  // Without these it fell back to el.getBBox() — the LOCAL, pre-transform box — so after a
  // scripted scale/rotate/translate (which append a `transform` attribute, never bake geometry)
  // getSelectionInfo()/expectSelection() reported the ORIGINAL box and tests like
  // scale-reference-point.jsx failed. We provide ONLY the read surface the oracle uses:
  //   • utilities.getBBox(el) → the element's LOCAL geometry bbox (the shim getBBox above).
  //   • math.getMatrix(el)    → its CONSOLIDATED matrix (doc root → node) from __scene.matrix.
  //   • math.transformBox(x,y,w,h,m) → the axis-aligned box of the local bbox's four corners
  //     pushed through m (the {aabox} shape test-hooks reads).
  // NOTE: `svgedit.history` is deliberately ABSENT — ai-dom guards every history use with
  //   `global.svgedit && svgedit.history`, so undo bookkeeping stays skipped (unchanged: a
  //   native run is one Transaction; there is no in-run undo stack). No other native path
  //   reads svgedit.utilities/math, so this only feeds the oracle.
  if (typeof global.svgedit === 'undefined') {
    global.svgedit = {
      utilities: {
        getBBox: function (el) { return (el && typeof el.getBBox === 'function') ? el.getBBox() : null; }
      },
      math: {
        getMatrix: function (el) {
          var a = (el && S.matrix) ? S.matrix(el.__id) : null;
          if (!a || a.length < 6) a = [1, 0, 0, 1, 0, 0];
          return { a: a[0], b: a[1], c: a[2], d: a[3], e: a[4], f: a[5] };
        },
        transformBox: function (x, y, w, h, m) {
          m = m || { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
          function ap(px, py) { return { x: m.a * px + m.c * py + m.e, y: m.b * px + m.d * py + m.f }; }
          var pts = [ap(x, y), ap(x + w, y), ap(x + w, y + h), ap(x, y + h)];
          var minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
          for (var i = 0; i < 4; i++) {
            minx = Math.min(minx, pts[i].x); miny = Math.min(miny, pts[i].y);
            maxx = Math.max(maxx, pts[i].x); maxy = Math.max(maxy, pts[i].y);
          }
          return { tl: pts[0], tr: pts[1], br: pts[2], bl: pts[3],
                   aabox: { x: minx, y: miny, width: maxx - minx, height: maxy - miny } };
        }
      }
    };
  }

  // ai-runner references `window.__SVGE_src` etc.; in JSC the global IS the window.
  if (typeof global.window === 'undefined') global.window = global;
})(typeof self !== 'undefined' ? self : this);
