'use strict';

const zlib = require('node:zlib');

/**
 * Minimal PDF 1.4 writer: pages, Helvetica/Helvetica-Bold/Courier text,
 * rects/lines, deflated content streams, RGB images as XObjects, and a
 * simple outline (bookmarks). Coordinates passed in are top-left based in
 * points; converted to PDF's bottom-left space internally.
 */

const FONTS = { F1: 'Helvetica', F2: 'Helvetica-Bold', F3: 'Courier' };
// Approximate average glyph width factors (per 1pt font size) for wrapping.
const FONT_WIDTH_FACTOR = { F1: 0.51, F2: 0.55, F3: 0.6 };

function esc(text) {
  return String(text).replace(/\\/g, '\\\\').replace(/\(/g, '\\(').replace(/\)/g, '\\)');
}

function toLatin1(text) {
  let out = '';
  for (const ch of String(text)) {
    const code = ch.codePointAt(0);
    out += code <= 0xff ? ch : '?';
  }
  return out;
}

function col(c) {
  return `${(c[0] / 255).toFixed(3)} ${(c[1] / 255).toFixed(3)} ${(c[2] / 255).toFixed(3)}`;
}

class PdfBuilder {
  constructor({ pageWidth = 595.28, pageHeight = 841.89 } = {}) {
    this.pageWidth = pageWidth;
    this.pageHeight = pageHeight;
    this.pages = [];
    this.images = []; // { name, width, height, data (deflated RGB), smask? }
    this.imageCache = new Map();
    this.bookmarks = []; // { title, pageIndex, y }
  }

  addPage() {
    this.pages.push({ ops: [] });
    return this.pages.length - 1;
  }

  get currentPage() {
    if (!this.pages.length) this.addPage();
    return this.pages[this.pages.length - 1];
  }

  textWidth(text, size, font = 'F1') {
    return String(text).length * size * (FONT_WIDTH_FACTOR[font] || 0.51);
  }

  /** Greedy word wrap to maxWidth points. */
  wrapText(text, size, maxWidth, font = 'F1') {
    const lines = [];
    for (const para of String(text).split('\n')) {
      const words = para.split(/\s+/).filter(Boolean);
      if (!words.length) { lines.push(''); continue; }
      let line = '';
      for (const word of words) {
        const candidate = line ? `${line} ${word}` : word;
        if (this.textWidth(candidate, size, font) <= maxWidth || !line) line = candidate;
        else { lines.push(line); line = word; }
      }
      lines.push(line);
    }
    return lines;
  }

  text(str, x, yTop, { size = 11, font = 'F1', color = [0, 0, 0] } = {}) {
    const y = this.pageHeight - yTop - size;
    this.currentPage.ops.push(
      `BT /${font} ${size} Tf ${col(color)} rg 1 0 0 1 ${x.toFixed(2)} ${y.toFixed(2)} Tm (${esc(toLatin1(str))}) Tj ET`
    );
  }

  rect(x, yTop, w, h, { fill = null, stroke = null, lineWidth = 1 } = {}) {
    const y = this.pageHeight - yTop - h;
    const ops = [];
    if (fill) ops.push(`${col(fill)} rg`);
    if (stroke) ops.push(`${col(stroke)} RG ${lineWidth} w`);
    ops.push(`${x.toFixed(2)} ${y.toFixed(2)} ${w.toFixed(2)} ${h.toFixed(2)} re`);
    ops.push(fill && stroke ? 'B' : fill ? 'f' : 'S');
    this.currentPage.ops.push(ops.join(' '));
  }

  line(x0, y0t, x1, y1t, { color = [0, 0, 0], width = 1 } = {}) {
    const y0 = this.pageHeight - y0t, y1 = this.pageHeight - y1t;
    this.currentPage.ops.push(
      `${col(color)} RG ${width} w ${x0.toFixed(2)} ${y0.toFixed(2)} m ${x1.toFixed(2)} ${y1.toFixed(2)} l S`
    );
  }

  /** Draw an RGBA raster image; alpha is dropped (composited upstream). */
  image(img, x, yTop, w, h) {
    let name = this.imageCache.get(img);
    if (!name) {
      name = `Im${this.images.length + 1}`;
      const rgb = Buffer.alloc(img.width * img.height * 3);
      for (let i = 0, n = img.width * img.height; i < n; i++) {
        rgb[i * 3] = img.data[i * 4];
        rgb[i * 3 + 1] = img.data[i * 4 + 1];
        rgb[i * 3 + 2] = img.data[i * 4 + 2];
      }
      this.images.push({ name, width: img.width, height: img.height, data: zlib.deflateSync(rgb) });
      this.imageCache.set(img, name);
    }
    const y = this.pageHeight - yTop - h;
    this.currentPage.ops.push(
      `q ${w.toFixed(2)} 0 0 ${h.toFixed(2)} ${x.toFixed(2)} ${y.toFixed(2)} cm /${name} Do Q`
    );
    return name;
  }

  bookmark(title, pageIndex = this.pages.length - 1) {
    this.bookmarks.push({ title: toLatin1(title), pageIndex });
  }

  build() {
    if (!this.pages.length) this.addPage();
    const objects = []; // 1-based; objects[i] = body string|Buffer after header
    const addObj = (body) => { objects.push(body); return objects.length; };

    // Reserve ids: 1 catalog, 2 pages tree (filled later)
    addObj(null); // 1: catalog placeholder
    addObj(null); // 2: pages placeholder

    const fontIds = {};
    for (const [res, base] of Object.entries(FONTS)) {
      fontIds[res] = addObj(`<< /Type /Font /Subtype /Type1 /BaseFont /${base} /Encoding /WinAnsiEncoding >>`);
    }
    const imageIds = {};
    for (const img of this.images) {
      imageIds[img.name] = addObj({
        dict: `<< /Type /XObject /Subtype /Image /Width ${img.width} /Height ${img.height} ` +
          `/ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /Length ${img.data.length} >>`,
        stream: img.data,
      });
    }

    const fontRes = Object.entries(fontIds).map(([r, id]) => `/${r} ${id} 0 R`).join(' ');
    const imgRes = this.images.map((img) => `/${img.name} ${imageIds[img.name]} 0 R`).join(' ');
    const resources = `<< /Font << ${fontRes} >> ${this.images.length ? `/XObject << ${imgRes} >>` : ''} >>`;

    const pageIds = [];
    for (const page of this.pages) {
      const content = zlib.deflateSync(Buffer.from(page.ops.join('\n'), 'latin1'));
      const contentId = addObj({
        dict: `<< /Filter /FlateDecode /Length ${content.length} >>`,
        stream: content,
      });
      pageIds.push(addObj(
        `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${this.pageWidth} ${this.pageHeight}] ` +
        `/Resources ${resources} /Contents ${contentId} 0 R >>`
      ));
    }

    let outlinesRef = '';
    if (this.bookmarks.length) {
      const outlineRootId = objects.length + 1;
      const itemIds = this.bookmarks.map((_, i) => outlineRootId + 1 + i);
      addObj(`<< /Type /Outlines /First ${itemIds[0]} 0 R /Last ${itemIds[itemIds.length - 1]} 0 R /Count ${itemIds.length} >>`);
      this.bookmarks.forEach((bm, i) => {
        const parts = [
          `/Title (${esc(bm.title)})`,
          `/Parent ${outlineRootId} 0 R`,
          `/Dest [${pageIds[bm.pageIndex]} 0 R /Fit]`,
        ];
        if (i > 0) parts.push(`/Prev ${itemIds[i - 1]} 0 R`);
        if (i < itemIds.length - 1) parts.push(`/Next ${itemIds[i + 1]} 0 R`);
        addObj(`<< ${parts.join(' ')} >>`);
      });
      outlinesRef = `/Outlines ${outlineRootId} 0 R /PageMode /UseOutlines`;
    }

    objects[0] = `<< /Type /Catalog /Pages 2 0 R ${outlinesRef} >>`;
    objects[1] = `<< /Type /Pages /Kids [${pageIds.map((id) => `${id} 0 R`).join(' ')}] /Count ${pageIds.length} >>`;

    // Serialize with xref
    const chunks = [Buffer.from('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n', 'latin1')];
    let offset = chunks[0].length;
    const offsets = [0];
    objects.forEach((body, idx) => {
      offsets.push(offset);
      let objBuf;
      if (body && typeof body === 'object' && body.stream) {
        objBuf = Buffer.concat([
          Buffer.from(`${idx + 1} 0 obj\n${body.dict}\nstream\n`, 'latin1'),
          body.stream,
          Buffer.from('\nendstream\nendobj\n', 'latin1'),
        ]);
      } else {
        objBuf = Buffer.from(`${idx + 1} 0 obj\n${body}\nendobj\n`, 'latin1');
      }
      chunks.push(objBuf);
      offset += objBuf.length;
    });

    const xrefStart = offset;
    let xref = `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
    for (let i = 1; i <= objects.length; i++) {
      xref += `${String(offsets[i]).padStart(10, '0')} 00000 n \n`;
    }
    xref += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefStart}\n%%EOF\n`;
    chunks.push(Buffer.from(xref, 'latin1'));
    return Buffer.concat(chunks);
  }
}

module.exports = { PdfBuilder, FONTS };
