import { Injectable } from '@angular/core';
import {
  PDFDocument,
  PDFName,
  PDFArray,
  rgb,
  StandardFonts,
} from 'pdf-lib';
import * as pdfjs from 'pdfjs-dist';
import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
import {
  Annotation,
  AnnotationForme,
  AnnotationImage,
  AnnotationLien,
  AnnotationTexte,
} from './annotation.model';

// Le worker pdf.js est copié dans /assets au build (cf. angular.json).
pdfjs.GlobalWorkerOptions.workerSrc = 'assets/pdf.worker.min.mjs';

/** Convertit un #rrggbb en composante rgb() de pdf-lib (0..1). */
function couleurVersRgb(hex: string) {
  const h = hex.replace('#', '');
  const r = parseInt(h.substring(0, 2), 16) / 255;
  const g = parseInt(h.substring(2, 4), 16) / 255;
  const b = parseInt(h.substring(4, 6), 16) / 255;
  return rgb(r || 0, g || 0, b || 0);
}

/**
 * Service de manipulation des PDF :
 * - rendu page par page avec pdf.js (pour l'affichage interactif) ;
 * - aplatissement des annotations dans le PDF avec pdf-lib (pour l'export).
 */
@Injectable({ providedIn: 'root' })
export class PdfService {
  /** Charge un document pdf.js depuis un ArrayBuffer. */
  async charger(data: ArrayBuffer): Promise<PDFDocumentProxy> {
    // On clone le buffer car pdf.js le « détache » (transférable).
    const copie = data.slice(0);
    return pdfjs.getDocument({ data: copie }).promise;
  }

  /** Charge une page pdf.js et retourne ses dimensions de base (échelle 1). */
  async chargerPage(
    pdf: PDFDocumentProxy,
    numeroPage: number,
  ): Promise<{ page: PDFPageProxy; baseLargeur: number; baseHauteur: number }> {
    const page = await pdf.getPage(numeroPage);
    const vp = page.getViewport({ scale: 1 });
    return { page, baseLargeur: vp.width, baseHauteur: vp.height };
  }

  /**
   * Dimensionne le canvas de façon SYNCHRONE à partir des dimensions de base et
   * du zoom (aucun await), et retourne la taille d'affichage (CSS px) à utiliser
   * pour la couche d'annotations. Bitmap en pixels physiques entiers, affichage
   * = bitmap/ratio (1:1, sans étirement) -> alignement canvas <-> overlay exact.
   *
   * Étant synchrone, la page prend sa bonne taille immédiatement au changement de
   * zoom : l'utilisateur ne peut pas dessiner sur une page encore à l'ancienne
   * taille (ce qui décalait les annotations sur les PDF lourds à rendu lent).
   */
  dimensionner(
    canvas: HTMLCanvasElement,
    baseLargeur: number,
    baseHauteur: number,
    echelle: number,
  ): { largeur: number; hauteur: number } {
    const ratio = window.devicePixelRatio || 1;
    const bitmapW = Math.round(baseLargeur * echelle * ratio);
    const bitmapH = Math.round(baseHauteur * echelle * ratio);
    canvas.width = bitmapW;
    canvas.height = bitmapH;
    const cssW = bitmapW / ratio;
    const cssH = bitmapH / ratio;
    canvas.style.width = `${cssW}px`;
    canvas.style.height = `${cssH}px`;
    return { largeur: cssW, hauteur: cssH };
  }

  /**
   * Démarre le rendu (rasterisation) de la page dans le canvas déjà dimensionné
   * et retourne la RenderTask pdf.js (qui expose `.promise` et `.cancel()`).
   * L'appelant doit annuler la tâche précédente avant d'en démarrer une nouvelle,
   * sinon des rendus concurrents (zoom rapide) se résolvent dans le désordre et
   * peignent un contenu incohérent avec la taille finale du canvas.
   */
  peindre(
    page: PDFPageProxy,
    canvas: HTMLCanvasElement,
    echelle: number,
  ): ReturnType<PDFPageProxy['render']> {
    const contexte = canvas.getContext('2d');
    if (!contexte) {
      throw new Error('Contexte 2D indisponible');
    }
    const ratio = window.devicePixelRatio || 1;
    const viewport = page.getViewport({ scale: echelle });
    // Le bitmap est en pixels ENTIERS (cf. dimensionner) alors que viewport est
    // fractionnaire : on étire très légèrement le rendu pour qu'il remplisse
    // EXACTEMENT le canvas -> alignement pixel-parfait contenu/couche.
    const sx = canvas.width / (viewport.width * ratio);
    const sy = canvas.height / (viewport.height * ratio);
    contexte.setTransform(ratio * sx, 0, 0, ratio * sy, 0, 0);
    return page.render({ canvasContext: contexte, viewport });
  }

  /**
   * Aplatit toutes les annotations dans le PDF source et retourne le binaire
   * résultant (Uint8Array), prêt au téléchargement ou à l'upload.
   *
   * @param sourcePdf binaire du PDF original
   * @param annotations annotations à appliquer (coordonnées en fraction de page)
   */
  async exporter(
    sourcePdf: ArrayBuffer,
    annotations: Annotation[],
  ): Promise<Uint8Array> {
    const doc = await PDFDocument.load(sourcePdf);
    const police = await doc.embedFont(StandardFonts.Helvetica);
    const pages = doc.getPages();

    // Pré-chargement des images embarquées (mutualisation par dataUrl).
    const cacheImages = new Map<string, Awaited<ReturnType<PDFDocument['embedPng']>>>();
    const embarquerImage = async (dataUrl: string) => {
      if (cacheImages.has(dataUrl)) {
        return cacheImages.get(dataUrl)!;
      }
      const octets = this.dataUrlVersOctets(dataUrl);
      const img = dataUrl.startsWith('data:image/png')
        ? await doc.embedPng(octets)
        : await doc.embedJpg(octets);
      cacheImages.set(dataUrl, img);
      return img;
    };

    for (const a of annotations) {
      const page = pages[a.page];
      if (!page) {
        continue;
      }
      const { width: pw, height: ph } = page.getSize();

      // Conversion fraction -> points PDF. En PDF l'origine est en bas-gauche,
      // alors que nos annotations sont en haut-gauche : on inverse l'axe Y.
      const x = a.x * pw;
      const largeur = a.w * pw;
      const hauteur = a.h * ph;
      const yHaut = ph - a.y * ph; // bord supérieur de l'annotation
      const yBas = yHaut - hauteur;

      switch (a.type) {
        case 'texte':
          this.dessinerTexte(page, a, police, x, yHaut);
          break;
        case 'image':
        case 'signature': {
          const img = await embarquerImage((a as AnnotationImage).dataUrl);
          page.drawImage(img, { x, y: yBas, width: largeur, height: hauteur });
          break;
        }
        case 'rectangle':
        case 'cercle':
        case 'ligne':
        case 'fleche':
        case 'croix':
          this.dessinerForme(page, a, x, yBas, yHaut, largeur, hauteur);
          break;
        case 'lien':
          this.dessinerLien(doc, page, a, x, yBas, largeur, hauteur);
          break;
      }
    }

    return doc.save();
  }

  // --- Rendu des différents types pour l'export --------------------------

  private dessinerTexte(
    page: ReturnType<PDFDocument['getPages']>[number],
    a: AnnotationTexte,
    police: Awaited<ReturnType<PDFDocument['embedFont']>>,
    x: number,
    yHaut: number,
  ): void {
    // taillePolice est exprimée en px à 100% : on l'exprime en points relatifs
    // à la hauteur de page (le rendu pdf.js à scale=1 ~ 1px = 1pt).
    const taille = a.taillePolice;
    const lignes = a.texte.split('\n');
    let y = yHaut - taille; // ligne de base de la première ligne
    for (const ligne of lignes) {
      page.drawText(ligne, {
        x,
        y,
        size: taille,
        font: police,
        color: couleurVersRgb(a.couleur),
      });
      y -= taille * 1.2;
    }
  }

  private dessinerForme(
    page: ReturnType<PDFDocument['getPages']>[number],
    a: AnnotationForme,
    x: number,
    yBas: number,
    yHaut: number,
    largeur: number,
    hauteur: number,
  ): void {
    const trait = couleurVersRgb(a.couleur);
    const remplissage =
      a.remplissage && a.remplissage !== 'none'
        ? couleurVersRgb(a.remplissage)
        : undefined;

    switch (a.type) {
      case 'rectangle':
        // Trait rentré dans la boîte (border-box) pour rester WYSIWYG avec
        // l'écran. À l'échelle 1, a.epaisseur est en points (pas de zoom).
        page.drawRectangle({
          x: x + a.epaisseur / 2,
          y: yBas + a.epaisseur / 2,
          width: Math.max(0, largeur - a.epaisseur),
          height: Math.max(0, hauteur - a.epaisseur),
          borderColor: trait,
          borderWidth: a.epaisseur,
          color: remplissage,
        });
        break;
      case 'cercle':
        page.drawEllipse({
          x: x + largeur / 2,
          y: yBas + hauteur / 2,
          xScale: Math.max(0, largeur / 2 - a.epaisseur / 2),
          yScale: Math.max(0, hauteur / 2 - a.epaisseur / 2),
          borderColor: trait,
          borderWidth: a.epaisseur,
          color: remplissage,
        });
        break;
      case 'ligne':
        page.drawLine({
          start: { x, y: yHaut },
          end: { x: x + largeur, y: yBas },
          thickness: a.epaisseur,
          color: trait,
        });
        break;
      case 'croix':
        // Deux diagonales formant un X dans la boîte englobante.
        page.drawLine({
          start: { x, y: yHaut },
          end: { x: x + largeur, y: yBas },
          thickness: a.epaisseur,
          color: trait,
        });
        page.drawLine({
          start: { x, y: yBas },
          end: { x: x + largeur, y: yHaut },
          thickness: a.epaisseur,
          color: trait,
        });
        break;
      case 'fleche': {
        const finX = x + largeur;
        const finY = yBas;
        page.drawLine({
          start: { x, y: yHaut },
          end: { x: finX, y: finY },
          thickness: a.epaisseur,
          color: trait,
        });
        // Pointe de flèche : deux petits segments.
        const angle = Math.atan2(finY - yHaut, finX - x);
        const taillePointe = 10 + a.epaisseur * 2;
        for (const delta of [Math.PI - 0.4, Math.PI + 0.4]) {
          page.drawLine({
            start: { x: finX, y: finY },
            end: {
              x: finX + taillePointe * Math.cos(angle + delta),
              y: finY + taillePointe * Math.sin(angle + delta),
            },
            thickness: a.epaisseur,
            color: trait,
          });
        }
        break;
      }
    }
  }

  private dessinerLien(
    doc: PDFDocument,
    page: ReturnType<PDFDocument['getPages']>[number],
    a: AnnotationLien,
    x: number,
    yBas: number,
    largeur: number,
    hauteur: number,
  ): void {
    // Rectangle visible (encadré bleu léger) pour matérialiser la zone cliquable.
    page.drawRectangle({
      x,
      y: yBas,
      width: largeur,
      height: hauteur,
      borderColor: couleurVersRgb('#3b6cff'),
      borderWidth: 1,
    });

    // Annotation de lien interactive (API bas niveau de pdf-lib).
    // ctx.obj convertit automatiquement une chaîne JS en PDFString, ce qui
    // produit une URI correctement encodée.
    const ctx = doc.context;
    const annot = ctx.obj({
      Type: 'Annot',
      Subtype: 'Link',
      Rect: [x, yBas, x + largeur, yBas + hauteur],
      Border: [0, 0, 0],
      A: ctx.obj({
        Type: 'Action',
        S: 'URI',
        URI: a.url,
      }),
    });
    const ref = ctx.register(annot);

    // Récupère le tableau Annots existant de la page ou en crée un.
    const cleAnnots = PDFName.of('Annots');
    let annots = page.node.lookupMaybe(cleAnnots, PDFArray);
    if (!annots) {
      annots = ctx.obj([]) as PDFArray;
      page.node.set(cleAnnots, annots);
    }
    annots.push(ref);
  }

  /** Décode une data URL base64 en Uint8Array. */
  private dataUrlVersOctets(dataUrl: string): Uint8Array {
    const base64 = dataUrl.split(',')[1] ?? '';
    const binaire = atob(base64);
    const octets = new Uint8Array(binaire.length);
    for (let i = 0; i < binaire.length; i++) {
      octets[i] = binaire.charCodeAt(i);
    }
    return octets;
  }
}
