/* ════════════════════════════════════════════════════════════════════
   pdf-engine.js — accès unifié aux deux moteurs :
   · PDF.js  (pdfjsLib)  → rendu des pages (vignettes, conversion en images)
   · pdf-lib (PDFLib, fork Cantoo) → écriture (fusion, extraction,
     rotation, chiffrement AES-128, déchiffrement)

   pdf.worker.min.js est chargé en <script> classique dans index.html :
   PDF.js détecte window.pdfjsWorker et travaille sur le thread principal,
   sans créer de Worker ni charger quoi que ce soit par le réseau — c'est
   ce qui permet à l'application de fonctionner ouverte en file://.
   ════════════════════════════════════════════════════════════════════ */
window.PdfEngine = (() => {
  'use strict';

  /** Erreur normalisée du moteur, avec message présentable. */
  class PdfError extends Error {
    /** @param {'ENCRYPTED'|'BAD_PASSWORD'|'INVALID'} code */
    constructor(code, message) {
      super(message);
      this.name = 'PdfError';
      this.code = code;
    }
  }

  function init() {
    // Filet de sécurité si la balise <script> du worker était retirée :
    // PDF.js retomberait alors sur ce chemin (utile en http://).
    if (window.pdfjsLib) {
      pdfjsLib.GlobalWorkerOptions.workerSrc = 'vendor/pdf.worker.min.js';
    }
  }

  // cMaps et polices standard embarquées : servent en http(s).
  // En file://, Chrome bloque leur lecture (fetch) ; PDF.js émet alors un
  // simple avertissement et le rendu continue — seuls de rares PDF à
  // polices CJK non intégrées peuvent s'afficher incomplètement.
  const PDFJS_OPTS = {
    cMapUrl: 'vendor/cmaps/',
    cMapPacked: true,
    standardFontDataUrl: 'vendor/standard_fonts/',
  };

  /* ── Ouverture avec PDF.js (lecture / rendu) ───────────────────── */

  /**
   * Ouvre un PDF avec PDF.js.
   * @param {Uint8Array} bytes — copiés en interne (PDF.js peut détacher le buffer).
   * @param {string} [password]
   * @returns {Promise<PDFDocumentProxy>}
   */
  async function openPdfJs(bytes, password) {
    try {
      return await pdfjsLib.getDocument({
        data: bytes.slice(),
        // ?? et non || : la chaîne vide est un mot de passe valable
        // (PDF protégés par un mot de passe propriétaire seul)
        password: password ?? undefined,
        ...PDFJS_OPTS,
      }).promise;
    } catch (e) {
      if (e && e.name === 'PasswordException') {
        // code 1 = mot de passe requis, code 2 = mot de passe incorrect
        throw new PdfError(e.code === 2 ? 'BAD_PASSWORD' : 'ENCRYPTED', e.message);
      }
      throw new PdfError('INVALID', 'Ce fichier ne semble pas être un PDF valide.');
    }
  }

  /**
   * Ouvre un PDF avec PDF.js en demandant le mot de passe à l'utilisateur
   * si nécessaire (boucle tant que le mot de passe est incorrect).
   * @returns {Promise<{doc:PDFDocumentProxy, password:string|null}|null>}
   *   null si l'utilisateur annule.
   */
  async function openPdfJsInteractive(bytes, fileName) {
    let password = null;
    let wrong = false;
    for (;;) {
      try {
        const doc = await openPdfJs(bytes, password);
        return { doc, password };
      } catch (e) {
        if (e.code !== 'ENCRYPTED' && e.code !== 'BAD_PASSWORD') throw e;
        wrong = e.code === 'BAD_PASSWORD' || wrong && password !== null;
        password = await Utils.promptPassword({ fileName, error: password !== null });
        if (password === null) return null; // annulé
      }
    }
  }

  /* ── Ouverture avec pdf-lib (écriture) ─────────────────────────── */

  /**
   * Ouvre un PDF avec pdf-lib (fork Cantoo), mot de passe accepté.
   * @param {Uint8Array} bytes
   * @param {string} [password]
   */
  async function openPdfLib(bytes, password) {
    try {
      return await PDFLib.PDFDocument.load(bytes, {
        password: password ?? undefined, // '' reste un mot de passe valable
        updateMetadata: false,
      });
    } catch (e) {
      const msg = String(e && e.message || '');
      if (e && e.constructor && e.constructor.name === 'EncryptedPDFError') {
        throw new PdfError('ENCRYPTED', 'Ce document est protégé par un mot de passe.');
      }
      if (/password/i.test(msg)) {
        throw new PdfError('BAD_PASSWORD', 'Mot de passe incorrect.');
      }
      throw new PdfError('INVALID', 'Ce fichier ne semble pas être un PDF valide.');
    }
  }

  /* ── File de rendu (vignettes et pages pleines) ────────────────── */
  // Le « worker » PDF.js tourne ici sur le thread principal : on limite
  // la concurrence pour garder l'interface fluide.
  const queue = [];
  let running = 0;
  const MAX_CONCURRENT = 2;

  function enqueue(job) {
    return new Promise((resolve, reject) => {
      queue.push({ job, resolve, reject });
      pump();
    });
  }
  async function pump() {
    if (running >= MAX_CONCURRENT || !queue.length) return;
    running++;
    const task = queue.shift();
    try { task.resolve(await task.job()); }
    catch (e) { task.reject(e); }
    finally { running--; pump(); }
  }

  /**
   * Rend une page en vignette nette (tient compte du devicePixelRatio).
   * @param {PDFDocumentProxy} doc
   * @param {number} pageIndex — index 0-based
   * @param {number} cssWidth — largeur d'affichage visée en px CSS
   * @param {number} [extraRotation] — rotation ajoutée par l'utilisateur (0/90/180/270)
   * @returns {Promise<HTMLCanvasElement>}
   */
  function renderThumb(doc, pageIndex, cssWidth, extraRotation = 0) {
    return enqueue(async () => {
      const page = await doc.getPage(pageIndex + 1);
      const rotation = (((page.rotate || 0) + extraRotation) % 360 + 360) % 360;
      const base = page.getViewport({ scale: 1, rotation });
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      const viewport = page.getViewport({ scale: (cssWidth / base.width) * dpr, rotation });
      const canvas = document.createElement('canvas');
      canvas.width = Math.ceil(viewport.width);
      canvas.height = Math.ceil(viewport.height);
      canvas.style.width = `${Math.round(viewport.width / dpr)}px`;
      await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
      return canvas;
    });
  }

  /**
   * Rend une page en taille réelle × `scale` (conversion en image).
   * scale = ppp / 72 (l'unité PDF vaut 1/72 de pouce).
   */
  function renderFull(doc, pageIndex, scale) {
    return enqueue(async () => {
      const page = await doc.getPage(pageIndex + 1);
      const viewport = page.getViewport({ scale, rotation: page.rotate || 0 });
      const canvas = document.createElement('canvas');
      canvas.width = Math.ceil(viewport.width);
      canvas.height = Math.ceil(viewport.height);
      await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
      return canvas;
    });
  }

  /* ── Rendu paresseux des vignettes ─────────────────────────────── */
  const pending = new Map(); // élément → callback
  const observer = ('IntersectionObserver' in window)
    ? new IntersectionObserver((entries) => {
        for (const entry of entries) {
          if (!entry.isIntersecting) continue;
          const cb = pending.get(entry.target);
          if (cb) {
            pending.delete(entry.target);
            observer.unobserve(entry.target);
            cb();
          }
        }
      }, { rootMargin: '300px' })
    : null;

  /** Exécute `cb` la première fois que `el` approche de l'écran. */
  function observe(el, cb) {
    if (observer) {
      pending.set(el, cb);
      observer.observe(el);
    } else {
      cb(); // navigateur sans IntersectionObserver : rendu immédiat
    }
  }

  /** Oublie un élément (carte supprimée avant d'avoir été rendue). */
  function unobserve(el) {
    if (observer && pending.has(el)) {
      pending.delete(el);
      observer.unobserve(el);
    }
  }

  return {
    PdfError, init,
    openPdfJs, openPdfJsInteractive, openPdfLib,
    renderThumb, renderFull, observe, unobserve,
  };
})();
