// pdf_convert_native.js — la conversione PDF → SVG vettoriale.
//
// È il gemello di `drawmac/js/PDFConvertNative.js` dell'app macOS (svgeditor@a2a033b7),
// portato riga per riga. **La logica non cambia**: cambiano solo i due punti in cui il
// Mac parla con WebKit, perché qui sotto non c'è una WKWebView ma QuickJS —
//
//   1. `step()` mandava le righe di avanzamento a
//      `window.webkit.messageHandlers.pdfconv`; qui vanno a `__pdfconv_log`, che è la
//      voce nativa registrata da pdf_import_pdfjs.cpp e finisce nello stesso posto;
//   2. il Mac riceveva i byte come base64 (era l'unico modo di passarli a
//      `callAsyncJavaScript`) e li ridecodificava; qui il C++ passa direttamente una
//      `Uint8Array`, quindi il giro in base64 — e il 33% di memoria in più che costava —
//      non c'è. `convert()` accetta ancora una stringa base64 per compatibilità con
//      l'oracolo, ma non è la via che usa il motore.
//
// Tutto il resto è identico e per una ragione sola: **l'SVG che esce di qui deve essere
// quello che esce dal Mac**. Ogni riga che si discosta è una riga da dimostrare; nessuna
// si discosta. In particolare restano invariati, con i loro commenti originali, i tre
// parametri che il Mac ha scoperto a caro prezzo — `forceDataSchema`, `embedFonts` e
// `isOffscreenCanvasSupported:false` — e la normalizzazione `normalize_for_native`.

var MD_PDFConvertNative = (function () {

  var dbg = [];
  function step(msg) {
    dbg.push(msg);
    try { __pdfconv_log(String(msg)); } catch (e) {}
  }

  function ensure_pdfjs_loaded() {
    if (typeof pdfjsLib === "undefined") {
      throw new Error("pdfjsLib not loaded (check scripting/pdfjs/pdf.js)");
    }
    // ⛔ NIENTE `GlobalWorkerOptions.workerSrc`. Il Mac lo imposta perché la WKWebView
    // può caricare `js/lib/pdf.worker.legacy.js` da file://; qui il worker è GIÀ nel
    // contesto — `pdf.worker.js` è stato valutato prima di `pdf.js` e ha definito
    // `globalThis.pdfjsWorker`, che pdf.js riconosce come "main thread worker message
    // handler" (pdf.js:4537) e usa senza chiedere niente a nessuno. Il risultato è lo
    // stesso fake worker in-processo su cui gira il Mac (pdf.js non può creare un vero
    // Worker da un'origine file://).
    if (typeof globalThis.pdfjsWorker === "undefined") {
      throw new Error("pdf.worker.js not loaded before pdf.js (no main-thread worker handler)");
    }
  }

  function base64_to_bytes(b64) {
    if (!b64 || typeof b64 !== "string") throw new Error("empty base64 input");
    var binary = atob(b64);
    var len = binary.length;
    var bytes = new Uint8Array(len);
    for (var i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
    return bytes;
  }

  function count_tag(svgEl, name) {
    try { return svgEl.getElementsByTagName(name).length; } catch (e) { return 0; }
  }

  // pdf.js' SVG back-end writes a few attributes that a BROWSER quietly ignores but that the
  // native scene renderer takes literally, because it has no CSS validity rules — it just
  // reads the attribute. Normalize exactly those, and nothing else:
  //
  //   • stroke-miterlimit="0" — SVGExtraState starts miterLimit at 0 and pdf.js emits it
  //     verbatim. In SVG a miterlimit < 1 is INVALID, so a browser falls back to the default
  //     (4); the native renderer instead honours 0, which turns every sharp corner into a
  //     bevel. Dropping the attribute reproduces the browser (and the fork).
  //   • empty presentation attributes (stroke-linecap="", stroke-dasharray="", transform="")
  //     — harmless but pure noise in a document the user will now edit and save.
  //
  // Lengths with a "px" suffix (stroke-width="4px") are deliberately LEFT ALONE: the native
  // length parser already ignores the unit, and 1px == 1 user unit here anyway.
  function normalize_for_native(svgEl) {
    var EMPTYABLE = ["stroke-linecap", "stroke-linejoin", "stroke-dasharray",
                     "stroke-dashoffset", "transform", "fill-rule", "clip-rule"];
    var fixed = 0;
    var all = svgEl.querySelectorAll("*");
    for (var i = 0; i < all.length; i++) {
      var el = all[i];
      for (var a = 0; a < EMPTYABLE.length; a++) {
        if (el.getAttribute(EMPTYABLE[a]) === "") { el.removeAttribute(EMPTYABLE[a]); fixed++; }
      }
      var ml = el.getAttribute("stroke-miterlimit");
      if (ml !== null && !(parseFloat(ml) >= 1)) { el.removeAttribute("stroke-miterlimit"); fixed++; }
    }
    return fixed;
  }

  function page_to_svg_string(pdf, pageNumber) {
    step("getPage " + pageNumber);
    return pdf.getPage(pageNumber).then(function (page) {
      // scale 1.0 = 1 PDF point per SVG user unit — the app's user unit, so a Letter page
      // converts to a 612×792 document exactly like the raster path sized its artboard.
      var viewport = page.getViewport({ scale: 1.0 });
      step("getOperatorList (viewport " + Math.round(viewport.width) + "x" + Math.round(viewport.height) + ")");
      return page.getOperatorList().then(function (opList) {
        step("opList ops=" + (opList && opList.fnArray ? opList.fnArray.length : "?"));
        // forceDataSchema:true is LOAD-BEARING here (3rd ctor argument). By default pdf.js'
        // createObjectURL returns a `blob:` URL for both the embedded raster images
        // (convertImgDataToPng) and the embedded font programs (addFontStyle). A blob: URL
        // is scoped to the document that created it, so it dies the moment the SVG string
        // leaves this converter — every <image> would resolve to nothing in the native scene
        // (which only decodes `data:` base64, see render.cpp's data-URI decoder) and the file
        // would not even survive a save. With forceDataSchema the same helper emits
        // `data:<mime>;base64,…` instead: self-contained bytes that travel with the document.
        var svgGfx = new pdfjsLib.SVGGraphics(page.commonObjs, page.objs, /*forceDataSchema*/ true);
        // embedFonts:true makes pdf.js emit the real embedded typeface as an @font-face
        // <style> block, which the native side reads to rewrite the generated family names
        // to the real PostScript ones (svgpdf::sfnt_postscript_name).
        svgGfx.embedFonts = true;
        step("loadDependencies");
        return svgGfx.loadDependencies(opList).then(function () {
          return svgGfx.getSVG(opList, viewport);
        }).then(function (svgEl) {
          if (!svgEl.getAttribute("width"))  svgEl.setAttribute("width",  Math.ceil(viewport.width));
          if (!svgEl.getAttribute("height")) svgEl.setAttribute("height", Math.ceil(viewport.height));
          // A viewBox is what lets the native parser map the content onto an artboard sized in
          // points even when pdf.js wrote the size with a unit suffix.
          if (!svgEl.getAttribute("viewBox")) {
            svgEl.setAttribute("viewBox", "0 0 " + viewport.width + " " + viewport.height);
          }
          // pdf.js (embedFonts) nests the @font-face CSS in <defs>. Hoist every <style> to be a
          // direct child of the root <svg>: the rules must be document-wide, and the native
          // font-name rewrite looks for them at the top level.
          var styles_hoisted = 0;
          try {
            var styles = svgEl.querySelectorAll("style");
            for (var si = 0; si < styles.length; si++) {
              if (styles[si].parentNode !== svgEl) { svgEl.appendChild(styles[si]); styles_hoisted++; }
            }
          } catch (e) { step("style hoist skipped: " + e); }

          var normalized = 0;
          try { normalized = normalize_for_native(svgEl); } catch (e) { step("normalize skipped: " + e); }

          var stats = {
            paths:     count_tag(svgEl, "path") + count_tag(svgEl, "svg:path"),
            texts:     count_tag(svgEl, "text") + count_tag(svgEl, "svg:text"),
            images:    count_tag(svgEl, "image") + count_tag(svgEl, "svg:image"),
            gradients: count_tag(svgEl, "linearGradient") + count_tag(svgEl, "svg:linearGradient") +
                       count_tag(svgEl, "radialGradient") + count_tag(svgEl, "svg:radialGradient"),
            patterns:  count_tag(svgEl, "pattern") + count_tag(svgEl, "svg:pattern"),
            clips:     count_tag(svgEl, "clipPath") + count_tag(svgEl, "svg:clipPath"),
            uses:      count_tag(svgEl, "use") + count_tag(svgEl, "svg:use"),
            styles:    styles_hoisted,
            normalized: normalized
          };

          var s = new XMLSerializer().serializeToString(svgEl);
          // pdf.js' SVGGraphics emits every element with an explicit "svg:" prefix
          // (<svg:g>, <svg:path>, …). The native scene parser matches UNPREFIXED names, so
          // drop the prefix and promote xmlns:svg to the default namespace. xlink: is
          // preserved (that is how pdf.js writes <image> hrefs and <use> references).
          s = s
            .replace(/xmlns:svg=(["'])http:\/\/www\.w3\.org\/2000\/svg\1/,
                     'xmlns="http://www.w3.org/2000/svg"')
            .replace(/<svg:/g, "<")
            .replace(/<\/svg:/g, "</");
          // Belt-and-braces on the forceDataSchema contract above: if any blob: reference
          // survived, the document would be broken OUTSIDE this page — say so instead of
          // shipping a silently empty image.
          stats.blobRefs = (s.match(/blob:/g) || []).length;
          step("svg serialized (vector), length=" + s.length +
               ", paths=" + stats.paths + " texts=" + stats.texts + " images=" + stats.images +
               (stats.blobRefs ? " WARNING blob: refs=" + stats.blobRefs : ""));
          return {
            svg: s,
            width: viewport.width,
            height: viewport.height,
            stats: stats
          };
        });
      });
    });
  }

  function open_document(input) {
    var bytes = (typeof input === "string") ? base64_to_bytes(input) : input;
    step("bytes=" + (bytes ? bytes.length : 0));
    if (!bytes || !bytes.length) throw new Error("0 bytes");
    return pdfjsLib.getDocument({
      data: bytes,
      fontExtraProperties: true,   // required by SVGGraphics to embed glyphs
      // LOAD-BEARING for embedded raster images. When OffscreenCanvas exists pdf.js decodes
      // images into an ImageBitmap, and the SVG back-end cannot consume one —
      // paintImageMaskXObject says so out loud ("ImageBitmap support is not implemented,
      // ensure that the `isOffscreenCanvasSupported` API parameter is disabled") and
      // paintImageXObject simply drops the image. With this false the images arrive as plain
      // pixel data and convertImgDataToPng turns them into the embedded PNG the document
      // needs. Measured on the gate PDF: 0 <image> before, 1 after.
      isOffscreenCanvasSupported: false,
      isEvalSupported: false,
      disableAutoFetch: true,
      disableStream: true
    });
  }

  // `input` = i byte grezzi del PDF (Uint8Array), oppure la loro base64 (la forma del
  // Mac). pageIndex è 1-based; <= 0 significa pagina 1.
  // Risolve con {ok:true, svg, width, height, pages, page, stats, debug} oppure
  // {ok:false, error, debug} — non RIGETTA MAI, così il chiamante nativo ha sempre un
  // verdetto invece di restare appeso a una promise caduta.
  function convert(input, pageIndex) {
    dbg = [];
    var pageNumber = pageIndex && pageIndex > 0 ? pageIndex : 1;
    return new Promise(function (resolve) {
      function fail(e, where) {
        var msg = (e && e.message) ? e.message : String(e);
        resolve({ ok: false, error: "[" + where + "] " + msg, debug: dbg.join("\n") });
      }
      try {
        step("start, page " + pageNumber);
        ensure_pdfjs_loaded();
        var loadingTask = open_document(input);
        step("getDocument task created");
        loadingTask.promise.then(function (pdf) {
          var n = Math.min(pageNumber, pdf.numPages);
          step("opened pdf, " + pdf.numPages + " pages, converting " + n);
          page_to_svg_string(pdf, n).then(function (out) {
            resolve({
              ok: true,
              svg: out.svg,
              width: out.width,
              height: out.height,
              pages: pdf.numPages,
              page: n,
              stats: out.stats,
              debug: dbg.join("\n")
            });
          }).catch(function (e) { fail(e, "convert"); });
        }).catch(function (e) { fail(e, "getDocument"); });
      } catch (e) { fail(e, "convert_pdf"); }
    });
  }

  // Quante pagine — senza convertirne nessuna. Serve al selettore di pagina, che il Mac
  // apre PRIMA della conversione (SVGEDocManager.pdf_open_page).
  function page_count(input) {
    dbg = [];
    return new Promise(function (resolve) {
      try {
        ensure_pdfjs_loaded();
        open_document(input).promise.then(function (pdf) {
          resolve({ ok: true, pages: pdf.numPages, debug: dbg.join("\n") });
        }).catch(function (e) {
          resolve({ ok: false, pages: 0, error: String(e && e.message ? e.message : e), debug: dbg.join("\n") });
        });
      } catch (e) {
        resolve({ ok: false, pages: 0, error: String(e && e.message ? e.message : e), debug: dbg.join("\n") });
      }
    });
  }

  return { convert: convert, page_count: page_count };
})();

// ─────────────────────────────────────────────────── il contratto col motore C++ ──
//
// Il Mac aspetta la promise con `callAsyncJavaScript`, che è una comodità di WebKit. Qui
// il chiamante è sincrono, quindi la forma è: si AVVIA il lavoro, e il motore gira la
// coda dei job finché `__pdfconv_out` non compare (pdf_import_pdfjs.cpp, `pump`).
// `convert()` non rigetta mai, quindi `__pdfconv_out` arriva sempre — o è il verdetto,
// o è {ok:false}.

globalThis.__pdfconv_out = null;

globalThis.__pdfconv_start = function (bytes, page) {
  globalThis.__pdfconv_out = null;
  MD_PDFConvertNative.convert(bytes, page).then(function (r) { globalThis.__pdfconv_out = r; });
};

globalThis.__pdfconv_pages_start = function (bytes) {
  globalThis.__pdfconv_out = null;
  MD_PDFConvertNative.page_count(bytes).then(function (r) { globalThis.__pdfconv_out = r; });
};

// Letto dal motore subito dopo il caricamento: se è false, la pila non è utilizzabile e
// File ▸ Import PDF… resta grigia con una ragione, invece di fallire a metà apertura.
globalThis.__pdfconv_ready = (typeof pdfjsLib !== "undefined" && typeof globalThis.pdfjsWorker !== "undefined");
