/* flux_zine_pdf.js — client-side FLUX zine PDF, locked to the canonical spec.
 *
 * Mirrors the ReportLab project zine (project_generator.py + generate_flux_issue.py)
 * 1:1: same constants, 46-page sequence, cover format, EXACT protocol page, photo
 * caption/header placement, effectiveGridH sizing, and manifest + QR. Coordinates
 * are computed in ReportLab space (y from bottom) and converted to jsPDF at draw
 * time, so positions match the canonical output as closely as jsPDF allows.
 *
 *   await FluxZinePDF.generate(orderedPhotos, zineMeta)   // build + download
 *   await FluxZinePDF.build(orderedPhotos, zineMeta)       // returns jsPDF doc
 *
 * orderedPhotos: 36 entries {id,large,thumb,seq,date,time,camera,address,gps,filename}
 *   (re-sorted chronologically here regardless of input order)
 * zineMeta: {slug,title,author,date,location,about,qrUrl}
 */
(function () {
  "use strict";

  // ── canonical constants (points; from generate_flux_issue.py) ──────────────
  var IN = 72, W = 792, H = 612;
  var MT = 0.85 * IN, MB = 0.85 * IN, INNER = 1.375 * IN, OUTER = 0.875 * IN;
  var IMG_AREA_W = W - INNER - OUTER;            // 8.75 in
  var IMG_AREA_H = H - MT - MB;                  // 6.80 in
  var GRID_IMG_H = IMG_AREA_H;
  var IMG_SCALE = 0.98;
  var CAP_Y = MB * 0.58, CAP_GAP = 9, CAP_SZ = 5.1;
  var HDR_Y = H - MT * 0.62, HDR_SZ = 4.8;
  var SPREAD_HDR_Y = H - (MT + 0.20 * IN), SPREAD_CONTENT_Y = H - (MT + 0.50 * IN);
  var PROTO_C1 = INNER, PROTO_C2 = 4.15 * IN, PROTO_C3 = 7.25 * IN;
  var PROTO_COL_W = 2.45 * IN, PROTO_TOP = H - 0.95 * IN, PROTO_LEAD = 0.135 * IN;
  var GENERATOR_URL = "https://flux.dantesisofo.com/generator/";

  // ── canonical staple guides — cover only (from generate_flux_issue.py /
  //    project_generator.py: STAPLE_X0/X1/UPPER/LOWER, color 0.82, lineWidth 0.25)
  //    RL y-from-bottom. 0.82*255 = 209 (jsPDF RGB 0-255).
  var STAPLE_X0 = 0.375 * IN, STAPLE_X1 = 0.625 * IN;
  var STAPLE_UPPER = H - 1.5 * IN, STAPLE_LOWER = 1.5 * IN;   // RL: 504, 108
  var STAPLE_GRAY = 209, STAPLE_LW = 0.25;

  function _JsPDF() { return (window.jspdf && window.jspdf.jsPDF) || window.jsPDF; }
  function margins(pageNo) { return pageNo % 2 === 1 ? [INNER, OUTER] : [OUTER, INNER]; } // recto=odd
  function pad2(n) { return ("0" + n).slice(-2); }

  function loadImg(src) {
    return new Promise(function (res, rej) {
      var im = new Image();
      im.onload = function () { res(im); };
      im.onerror = function () { rej(new Error("image load failed: " + src)); };
      im.src = src; // same-origin → no canvas taint
    });
  }
  function qrDataURL(text, px) {
    return new Promise(function (res) {
      if (!window.QRCode || !text) return res(null);
      var d = document.createElement("div"); d.style.cssText = "position:absolute;left:-9999px";
      document.body.appendChild(d);
      try { new QRCode(d, { text: text, width: px, height: px, correctLevel: QRCode.CorrectLevel.M }); } catch (e) {}
      setTimeout(function () {
        var c = d.querySelector("canvas"), img = d.querySelector("img");
        var out = c ? c.toDataURL("image/png") : (img ? img.src : null);
        document.body.removeChild(d); res(out);
      }, 80);
    });
  }

  function fmtDto(date, time) {
    var d = (date || "").trim(), t = (time || "").trim();
    return d && t ? d + "  " + t : (d || t);
  }
  function seqNum(p) { return parseInt(p.seq, 10) || 0; }

  async function build(orderedIn, meta) {
    meta = meta || {};
    var JsPDF = _JsPDF(); if (!JsPDF) throw new Error("jsPDF not loaded");
    var ordered = orderedIn.slice().sort(function (a, b) { return seqNum(a) - seqNum(b); }); // chronological, always
    var doc = new JsPDF({ orientation: "landscape", unit: "pt", format: "letter" });

    // RL→jsPDF helpers (rly = baseline from bottom)
    function tx(x, rly, s, o) { doc.text(String(s == null ? "" : s), x, H - rly, o); }
    function ln(x1, r1, x2, r2) { doc.line(x1, H - r1, x2, H - r2); }
    function rule(x, rly, w) { doc.setDrawColor(133, 133, 133); doc.setLineWidth(0.55); ln(x, rly, x + (w || PROTO_COL_W), rly); }
    function F(style, size) { doc.setFont("courier", style); doc.setFontSize(size); }
    function col(c) { doc.setTextColor(c, c, c); }
    function wrap(text, style, size, maxW) { F(style, size); return doc.splitTextToSize(String(text || ""), maxW); }
    function fitFont(text, style, start, maxW, min) {
      var s = start; doc.setFont("courier", style);
      while (s > min) { doc.setFontSize(s); if (doc.getTextWidth(String(text || "")) <= maxW) break; s -= 0.5; }
      doc.setFontSize(s); return s;
    }

    // preload images + effectiveGridH (uniform height floor)
    // Offline (file://) support: prefer a pre-bundled base64 data-URI when present
    // (window.FLUX_ZINE_IMAGES, keyed by photo id). A data-URI <img> is origin-clean,
    // so jsPDF's internal canvas is NOT tainted under file://. On a server/host the map
    // is absent and we fall back to the large/thumb file path (unchanged behavior).
    var IMGMAP = window.FLUX_ZINE_IMAGES || null;
    function srcFor(p) {
      return (IMGMAP && p && IMGMAP[p.id]) || p.large || p.thumb;
    }
    var imgs = [];
    for (var i = 0; i < ordered.length; i++) { try { imgs.push(await loadImg(srcFor(ordered[i]))); } catch (e) { imgs.push(null); } }
    var eg = GRID_IMG_H;
    imgs.forEach(function (im) { if (im) { var fit = (IMG_AREA_W / im.naturalWidth) * im.naturalHeight; if (fit < eg) eg = fit; } });

    // Pre-generate QR images up front (async) so page drawing is fully synchronous
    // and each QR lands on the correct page (walk QR = info + manifest; generator
    // QR = protocol page only).
    var walkQR = await qrDataURL(meta.qrUrl, 120);
    var genQR = await qrDataURL(GENERATOR_URL, 120);

    // ── p1 COVER (recto) — title verbatim 13b, date 9 #222, photographers 9 #222
    var cm = margins(1)[0];
    var ty = H - MT - 2;
    var coverLines = [[meta.title || "", "bold", 13, 0], [meta.date || "", "normal", 9, 34],
                      [meta.author || "", "normal", 9, 34]];
    coverLines.forEach(function (cl) {
      if (cl[0]) { F(cl[1], cl[2]); col(cl[3]); tx(cm, ty, cl[0]); }
      ty -= cl[2] + 5;
    });
    // Staple guides — cover only (canonical: 2 horizontal marks at left spine)
    doc.setDrawColor(STAPLE_GRAY, STAPLE_GRAY, STAPLE_GRAY);
    doc.setLineWidth(STAPLE_LW);
    ln(STAPLE_X0, STAPLE_UPPER, STAPLE_X1, STAPLE_UPPER);
    ln(STAPLE_X0, STAPLE_LOWER, STAPLE_X1, STAPLE_LOWER);

    // ── p2 blank ──────────────────────────────────────────────────────────────
    doc.addPage();

    // ── p3 INFO / WALK page (recto) + QR(walk) ─────────────────────────────────
    doc.addPage();
    (function () {
      var mp = margins(3), ml = mp[0], mr = mp[1], textW = W - ml - mr, y = H - MT;
      F("normal", 5.5); col(120); tx(ml, y, "FLUX WALK");
      y -= 0.18 * IN; doc.setDrawColor(0, 0, 0); doc.setLineWidth(0.75); ln(ml, y, W - mr, y);
      y -= 0.45 * IN;
      var tsize = fitFont(meta.title || "", "bold", 18, textW, 9);
      var tl = wrap(meta.title || "", "bold", tsize, textW);
      F("bold", tsize); col(0); tl.forEach(function (L) { tx(ml, y, L); y -= tsize * 1.35; });
      y -= 0.18 * IN; doc.setDrawColor(191, 191, 191); doc.setLineWidth(0.3); ln(ml, y, W - mr, y); y -= 0.22 * IN;
      var parts = [meta.date, meta.location].filter(Boolean);
      if (parts.length) { F("normal", 8.5); col(0); tx(ml, y, parts.join("  /  ")); y -= 0.26 * IN; }
      if (meta.author) {
        y -= 0.10 * IN; F("normal", 6); col(100); tx(ml, y, "PHOTOGRAPHY"); y -= 0.18 * IN;
        F("normal", 8.5); col(0); tx(ml, y, meta.author); y -= 0.19 * IN;
      }
      if (meta.about) {
        y -= 0.18 * IN; F("normal", 6); col(100); tx(ml, y, "DESCRIPTION"); y -= 0.18 * IN;
        var dl = wrap(meta.about, "normal", 7.5, textW); F("normal", 7.5); col(0);
        for (var j = 0; j < dl.length; j++) { if (y < MB + 0.6 * IN) break; tx(ml, y, dl[j]); y -= 0.165 * IN; }
      }
      drawQR(walkQR, mr, "SCAN TO VIEW", "WALK ONLINE");
    })();

    // ── p4 blank ──────────────────────────────────────────────────────────────
    doc.addPage();

    // ── p5 PROTOCOL (recto) — EXACT canonical content ─────────────────────────
    doc.addPage();
    protocolPage();

    // ── p6 blank ──────────────────────────────────────────────────────────────
    doc.addPage();

    // ── p7..p42 PHOTOS (chronological; first on recto, last on verso) ─────────
    var label = meta.slug || "flux";
    for (var k = 0; k < ordered.length; k++) {
      doc.addPage();
      var pageNo = doc.getNumberOfPages();
      var pm = margins(pageNo), ml = pm[0], mr = pm[1];
      var p = ordered[k], im = imgs[k];
      if (im) {
        var sc = Math.min(IMG_AREA_W / im.naturalWidth, eg / im.naturalHeight);
        var dw = im.naturalWidth * sc * IMG_SCALE, dh = im.naturalHeight * sc * IMG_SCALE;
        var ix = ml + (IMG_AREA_W - dw) / 2;
        var iyBottom = MB + (GRID_IMG_H - dh) / 2;            // centre at PAGE_H/2
        doc.addImage(im, "JPEG", ix, H - (iyBottom + dh), dw, dh, undefined, "FAST");
      }
      // Location priority: human-readable address first; coordinates secondary/fallback.
      var loc = (p.address || "").trim();
      var coords = (p.gps || "").trim();
      var cap1 = fmtDto(p.date, p.time) + (loc ? "    " + loc : (coords ? "    " + coords : ""));
      var cap2 = (meta.author || "") + (loc && coords ? "    " + coords : "");
      F("normal", CAP_SZ); col(120);
      tx(ml, CAP_Y + CAP_GAP, cap1);
      tx(ml, CAP_Y, cap2);
      F("normal", HDR_SZ); col(150);
      tx(W - mr, HDR_Y, label + "  " + pad2(k + 1) + "/36", { align: "right" });
    }

    // ── p43 blank ─────────────────────────────────────────────────────────────
    doc.addPage();

    // ── p44 CONTACT SHEET (verso/left) — 6×6 in frame order ───────────────────
    doc.addPage();
    (function () {
      var pm = margins(doc.getNumberOfPages()), ml = pm[0];
      F("bold", 8); col(0); tx(ml, SPREAD_HDR_Y, (meta.title || label) + "  —  CONTACT SHEET");
      var cols = 6, rows = 6, gap = 0.055 * IN, numH = 0.14 * IN;
      var gridTop = SPREAD_CONTENT_Y, gridBottomY = MB;      // RL
      var cellW = (IMG_AREA_W - gap * (cols - 1)) / cols;
      var areaH = gridTop - gridBottomY;
      var cellH = (areaH - gap * (rows - 1)) / rows;
      for (var c = 0; c < ordered.length; c++) {
        var r = Math.floor(c / cols), cc = c % cols;
        var cx = ml + cc * (cellW + gap);
        var cellTop = gridTop - r * (cellH + gap);            // RL top of cell
        var im = imgs[c];
        if (im) {
          var aw = cellW, ah = cellH - numH;
          var sc = Math.min(aw / im.naturalWidth, ah / im.naturalHeight);
          var dw = im.naturalWidth * sc, dh = im.naturalHeight * sc;
          var dx = cx + (cellW - dw) / 2;
          var imgTopRL = cellTop - (ah - dh) / 2;             // image in top area; numH reserved at bottom for number
          doc.addImage(im, "JPEG", dx, H - imgTopRL, dw, dh, undefined, "FAST");
        }
        F("normal", 4.6); col(120);
        tx(cx, cellTop - cellH + 3, pad2(c + 1));
      }
    })();

    // ── p45 MANIFEST (recto/right) + QR(walk) ─────────────────────────────────
    doc.addPage();
    (function () {
      var pm = margins(doc.getNumberOfPages()), ml = pm[0], mr = pm[1];
      F("bold", 8); col(0); tx(ml, SPREAD_HDR_Y, (meta.title || label) + "  —  MANIFEST");
      var colW = (IMG_AREA_W - 0.50 * IN) / 2;
      var colX = [ml, ml + colW + 0.50 * IN];
      var y0 = SPREAD_CONTENT_Y;
      // Manifest entries WRAP onto indented continuation lines so the full
      // human-readable address is always visible — no character truncation.
      // Mirrors the canonical generate_flux_issue.draw_manifest_page() wrapping:
      // 7pt Courier, two columns, single-line entries keep the old 0.195in spacing
      // (contStep + entryGap), columns balanced by total line count.
      var SZ = 7.0, padR = 0.06 * IN, contStep = 0.135 * IN, entryGap = 0.060 * IN;
      F("normal", SZ);
      var hang = doc.getTextWidth("000  ");          // continuation-line indent
      function entryLines(p, idx) {
        var tsStr = (p.date ? p.date + (p.time ? "  " + (p.time.length >= 5 ? p.time.slice(0, 5) : p.time) : "") : "—");
        var base = ("00" + (idx + 1)).slice(-3) + "  " + tsStr + "  " + (meta.author || "");
        var addr = (p.address || "").trim() || (p.gps || "");
        if (!addr) return [base];
        var prefix = base + "  —  ";
        F("normal", SZ);
        var headMax = colW - padR - doc.getTextWidth(prefix);   // room for addr on line 0
        var contMax = colW - padR - hang;                       // room on wrapped lines
        var words = addr.split(/\s+/), cur = "", rest = [];
        for (var k = 0; k < words.length; k++) {
          var trial = cur ? cur + " " + words[k] : words[k];
          if (!cur || doc.getTextWidth(trial) <= headMax) { cur = trial; }
          else { rest = words.slice(k); break; }
        }
        var lines = [prefix + cur];
        if (rest.length) lines = lines.concat(doc.splitTextToSize(rest.join(" "), contMax));
        return lines;
      }
      var entries = [];
      for (var i2 = 0; i2 < ordered.length; i2++) entries.push(entryLines(ordered[i2], i2));
      var totalLines = 0; for (var e0 = 0; e0 < entries.length; e0++) totalLines += entries[e0].length;
      var target0 = Math.ceil(totalLines / 2), splitAt = entries.length, acc = 0;
      for (var e1 = 0; e1 < entries.length; e1++) { acc += entries[e1].length; if (acc >= target0) { splitAt = e1 + 1; break; } }
      var cols = [entries.slice(0, splitAt), entries.slice(splitAt)];
      F("normal", SZ); col(0);
      for (var c2 = 0; c2 < cols.length; c2++) {
        var x = colX[c2], y = y0;
        for (var e2 = 0; e2 < cols[c2].length; e2++) {
          var L = cols[c2][e2];
          tx(x, y, L[0]);
          for (var k2 = 1; k2 < L.length; k2++) tx(x + hang, y - k2 * contStep, L[k2]);
          y -= L.length * contStep + entryGap;
        }
      }
      drawQR(walkQR, mr, "SCAN TO VIEW", "WALK ONLINE");
      F("normal", 5.5); col(40); tx(ml, MB * 0.45, "FLUX_ISSUE_PROTOCOL_v2.0  —  " + label + "  —  " + (meta.author || ""));
    })();

    // ── p46 blank back cover ──────────────────────────────────────────────────
    doc.addPage();

    return doc;

    // ── shared: QR bottom-right (walk URL) ────────────────────────────────────
    function drawQR(data, mr, l1, l2) {
      if (!data) return;
      var QR = 0.62 * IN, qx = W - mr - QR, qyBottom = MB * 0.40;
      F("normal", 4.5); col(120);
      tx(qx + QR, qyBottom + QR + 8, l1, { align: "right" });
      tx(qx + QR, qyBottom + QR + 2, l2, { align: "right" });
      doc.addImage(data, "PNG", qx, H - (qyBottom + QR), QR, QR);
    }

    // ── EXACT canonical protocol page (3 columns) ─────────────────────────────
    function protocolPage() {
      function bodyBlock(lines, x, y) {
        for (var i = 0; i < lines.length; i++) {
          var line = lines[i];
          if (line) {
            var strong = line.charAt(line.length - 1) === ":" ||
              (line === line.toUpperCase() && line.length > 0 && line.length < 28 &&
               line.charAt(0) !== "-" && line.charAt(0) !== " ");
            F(strong ? "bold" : "normal", 6.45); col(0); tx(x, y, line);
          }
          y -= PROTO_LEAD;
        }
        return y;
      }
      function stepHdr(x, y, step, title) {
        rule(x, y); F("bold", 6.2); col(120); tx(x, y - 0.16 * IN, step);
        F("bold", 8.8); col(0); tx(x, y - 0.31 * IN, title); rule(x, y - 0.43 * IN); return y - 0.73 * IN;
      }
      function mainHdr(x, y, title) {
        rule(x, y); F("bold", 8.8); col(0); tx(x, y - 0.25 * IN, title); rule(x, y - 0.37 * IN); return y - 0.67 * IN;
      }
      var y;
      // Column 1
      y = mainHdr(PROTO_C1, PROTO_TOP, "WHAT IS FLUX");
      y = bodyBlock(["FLUX is an open-source chronological", "photography publishing system.", "",
        "36 photographs become one", "complete numbered issue.", "", "The system generates:",
        "- PDF publication", "- contact sheet", "- metadata manifest", "- printable issue"], PROTO_C1, y);
      y = stepHdr(PROTO_C1, y - 0.12 * IN, "STEP_01", "CAPTURE");
      y = bodyBlock(["CAMERA: compact digital camera", "FILE TYPE: small JPEG",
        "COLOR MODE: high contrast", "monochrome", "", "PURPOSE:", "  eliminate processing",
        "  eliminate workflow friction", "  maximize speed"], PROTO_C1, y);
      y = stepHdr(PROTO_C1, y - 0.12 * IN, "STEP_02", "SELECT");
      bodyBlock(["SELECT 36 PHOTOGRAPHS", "", "REVIEW PHOTOGRAPHS", "AS SMALL THUMBNAILS", "",
        "MAKE FINAL SELECTIONS", "QUICKLY", "", "DO NOT:", "- overedit", "- oversequence", "- overanalyze"], PROTO_C1, y);
      // Column 2
      y = stepHdr(PROTO_C2, PROTO_TOP, "STEP_03", "UPLOAD");
      y = bodyBlock(["UPLOAD 36 JPEG FILES AT:", "", "flux.dantesisofo.com", "/generator/"], PROTO_C2, y);
      y = stepHdr(PROTO_C2, y - 0.12 * IN, "STEP_04", "SYSTEM AUTOMATION");
      y = bodyBlock(["SYSTEM AUTOMATICALLY:", "  - reads timestamps", "  - preserves chronological order",
        "  - generates derivatives", "  - creates metadata", "  - generates contact sheet",
        "  - generates issue number", "  - generates issue PDF", "  - compresses for sharing", "",
        "OUTPUT:", "downloadable issue PDF"], PROTO_C2, y);
      y = mainHdr(PROTO_C2, y - 0.12 * IN, "ISSUE STRUCTURE");
      bodyBlock(["DIGITAL ISSUE INCLUDES:", "  - chronological photo pages", "  - 6x6 contact sheet",
        "  - metadata manifest", "  - printable PDF", "", "ALL PHOTOGRAPHS REMAIN", "IN CHRONOLOGICAL ORDER"], PROTO_C2, y);
      // Column 3
      y = stepHdr(PROTO_C3, PROTO_TOP, "STEP_05", "ISSUE GENERATION");
      y = bodyBlock(["SYSTEM AUTOMATICALLY:", "- reads timestamps", "- sequences photographs",
        "- builds complete PDF", "", "NO MANUAL LAYOUT"], PROTO_C3, y);
      y = stepHdr(PROTO_C3, y - 0.12 * IN, "STEP_06", "PRINT");
      y = bodyBlock(["PRINT: double sided", "PAPER: 8.5 x 11 office paper", "", "STACK PAGES",
        "ALIGN EDGES", "DO NOT FOLD", "STAPLE LEFT SIDE", "USING COVER MARKS"], PROTO_C3, y);
      y = stepHdr(PROTO_C3, y - 0.12 * IN, "STEP_07", "DISTRIBUTE");
      y = bodyBlock(["PRINT", "DISTRIBUTE", "", "STORE", "IN MANILA FOLDERS", "CHRONOLOGICALLY", "",
        "ARCHIVE", "REPEAT"], PROTO_C3, y);
      rule(PROTO_C3, y - 0.15 * IN); F("normal", 6.45); col(0); tx(PROTO_C3, y - 0.32 * IN, "END_OF_PROTOCOL");
      rule(PROTO_C3, y - 0.42 * IN);
      F("normal", 4.5); col(120);
      tx(PROTO_C3, y - 0.56 * IN, "SCAN TO CREATE"); tx(PROTO_C3, y - 0.69 * IN, "YOUR OWN FLUX ISSUE");
      var QR = 0.62 * IN, qyBottom = y - 0.87 * IN - QR;   // RL bottom (canonical)
      if (genQR) doc.addImage(genQR, "PNG", PROTO_C3, H - (qyBottom + QR), QR, QR);
      F("normal", 5.5); col(40); tx(PROTO_C1, MB * 0.45, "FLUX_ISSUE_PROTOCOL_v2.0");
    }
  }

  async function generate(ordered, meta) {
    var doc = await build(ordered, meta);
    doc.save(((meta && meta.slug) || "flux") + "_zine.pdf");
    return true;
  }

  window.FluxZinePDF = { build: build, generate: generate };
})();
