import {
  AfterViewInit,
  Component,
  ElementRef,
  EventEmitter,
  Input,
  OnChanges,
  Output,
  SimpleChanges,
  ViewChild,
  inject,
} from '@angular/core';
import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
import { PdfService } from './pdf.service';
import {
  Annotation,
  AnnotationForme,
  AnnotationImage,
  AnnotationLien,
  AnnotationTexte,
  Outil,
} from './annotation.model';

/** Identifiant unique simple pour les annotations. */
function nouvelId(): string {
  return Math.random().toString(36).slice(2, 10);
}

/**
 * Affiche UNE page de PDF (canvas pdf.js) surmontée d'une couche d'édition
 * interactive (DOM positionné en absolu).
 *
 * Choix d'implémentation : couche DOM/CSS maison plutôt qu'une lib type
 * fabric/konva. Les éléments à manipuler restent simples (texte, image, formes,
 * liens) et le DOM offre nativement l'édition de texte, le rendu d'images et la
 * sélection ; cela évite une dépendance lourde et garde le code lisible.
 */
@Component({
  selector: 'app-page-layer',
  standalone: true,
  template: `
    <div
      class="page"
      #conteneur
      [style.width.px]="largeur"
      [style.height.px]="hauteur"
      (mousedown)="debutInteraction($event)"
    >
      <canvas #canvas class="rendu"></canvas>

      <!-- Couche d'édition -->
      <div class="couche">
        <!-- DEBUG TEMP : repère BLEU (DOM) 30%-70% -->
        <div style="position:absolute; left:30%; top:30%; width:40%; height:40%; border:2px solid blue; pointer-events:none; box-sizing:border-box;"></div>
        @if (pageIndex === 0) {
          <div style="position:fixed; top:96px; left:8px; z-index:9999; background:#ff0; color:#000; font:11px monospace; padding:4px 6px; border:1px solid #000; pointer-events:none; white-space:pre;">{{ dbg }}</div>
        }
        @for (a of annotationsPage(); track a.id) {
          <div
            class="annotation"
            [class.selectionnee]="a.id === selectionId"
            [style.left.px]="a.x * largeur"
            [style.top.px]="a.y * hauteur"
            [style.width.px]="a.w * largeur"
            [style.height.px]="a.h * hauteur"
            (mousedown)="debutDeplacement($event, a)"
          >
            @switch (a.type) {
              @case ('texte') {
                <div
                  class="contenu-texte"
                  [class.edition]="a.id === editionId"
                  [attr.contenteditable]="a.id === editionId"
                  [style.color]="texte(a).couleur"
                  [style.fontSize.px]="texte(a).taillePolice * zoom"
                  (dblclick)="entrerEdition(a, $event)"
                  (blur)="finEdition(a, $event)"
                  (mousedown)="a.id === editionId && $event.stopPropagation()"
                >{{ texte(a).texte }}</div>
              }
              @case ('image') {
                <img class="contenu-image" [src]="image(a).dataUrl" alt="" />
              }
              @case ('signature') {
                <img class="contenu-image" [src]="image(a).dataUrl" alt="signature" />
              }
              @case ('lien') {
                <div class="contenu-lien" [title]="lien(a).url">🔗 {{ lien(a).url }}</div>
              }
              @default {
                <!-- formes : rectangle / cercle / ligne / flèche dessinées en SVG -->
                <svg class="contenu-forme" [attr.viewBox]="'0 0 ' + (a.w * largeur) + ' ' + (a.h * hauteur)" preserveAspectRatio="none">
                  @if (a.type === 'rectangle') {
                    <!-- Trait rentré dans la boîte (box-sizing: border-box) pour
                         que le rendu colle exactement au curseur / à la sélection. -->
                    <rect [attr.x]="rectInset(a).x" [attr.y]="rectInset(a).y"
                      [attr.width]="rectInset(a).width" [attr.height]="rectInset(a).height"
                      [attr.stroke]="forme(a).couleur" [attr.stroke-width]="strokeW(a)"
                      [attr.fill]="forme(a).remplissage || 'none'" />
                  } @else if (a.type === 'cercle') {
                    <ellipse [attr.cx]="ellipseInset(a).cx" [attr.cy]="ellipseInset(a).cy"
                      [attr.rx]="ellipseInset(a).rx" [attr.ry]="ellipseInset(a).ry"
                      [attr.stroke]="forme(a).couleur" [attr.stroke-width]="strokeW(a)"
                      [attr.fill]="forme(a).remplissage || 'none'" />
                  } @else if (a.type === 'ligne') {
                    <line x1="0" y1="0" [attr.x2]="a.w*largeur" [attr.y2]="a.h*hauteur"
                      [attr.stroke]="forme(a).couleur" [attr.stroke-width]="forme(a).epaisseur * zoom" />
                  } @else if (a.type === 'fleche') {
                    <line x1="0" y1="0" [attr.x2]="a.w*largeur" [attr.y2]="a.h*hauteur"
                      [attr.stroke]="forme(a).couleur" [attr.stroke-width]="forme(a).epaisseur * zoom"
                      [attr.marker-end]="'url(#fleche-' + a.id + ')'" />
                    <defs>
                      <marker [attr.id]="'fleche-' + a.id" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
                        <path d="M0,0 L8,3 L0,6 Z" [attr.fill]="forme(a).couleur" />
                      </marker>
                    </defs>
                  } @else if (a.type === 'croix') {
                    <line x1="0" y1="0" [attr.x2]="a.w*largeur" [attr.y2]="a.h*hauteur"
                      [attr.stroke]="forme(a).couleur" [attr.stroke-width]="forme(a).epaisseur * zoom" />
                    <line [attr.x1]="a.w*largeur" y1="0" x2="0" [attr.y2]="a.h*hauteur"
                      [attr.stroke]="forme(a).couleur" [attr.stroke-width]="forme(a).epaisseur * zoom" />
                  }
                </svg>
              }
            }

            @if (a.id === selectionId) {
              @if (a.type === 'texte') {
                <div class="barre-texte" (mousedown)="$event.stopPropagation()">
                  <button class="mini" (click)="changerTaille(a, -2)" title="Réduire">A−</button>
                  <input
                    class="champ-taille"
                    type="number"
                    min="6"
                    max="300"
                    [value]="texte(a).taillePolice"
                    (input)="majTaille(a, $any($event.target).value)"
                  />
                  <button class="mini" (click)="changerTaille(a, 2)" title="Agrandir">A+</button>
                </div>
              }
              <button class="poignee-suppr" (mousedown)="$event.stopPropagation()" (click)="supprimer(a)">×</button>
              <span class="poignee-resize" (mousedown)="debutRedim($event, a)"></span>
            }
          </div>
        }
      </div>
    </div>
  `,
  styles: [
    `
      .page {
        position: relative;
        margin: 0 auto 24px;
        background: #fff;
        box-shadow: var(--ombre);
      }
      .rendu {
        display: block;
      }
      .couche {
        position: absolute;
        inset: 0;
      }
      .annotation {
        position: absolute;
        box-sizing: border-box;
        cursor: move;
      }
      .annotation.selectionnee {
        outline: 1px dashed var(--couleur-primaire);
      }
      .contenu-texte {
        width: 100%;
        height: 100%;
        outline: none;
        white-space: pre-wrap;
        overflow: hidden;
        cursor: move;
      }
      .contenu-texte.edition {
        cursor: text;
      }
      .contenu-image {
        width: 100%;
        height: 100%;
        object-fit: contain;
        pointer-events: none;
      }
      .contenu-forme {
        width: 100%;
        height: 100%;
        overflow: visible;
        pointer-events: none;
      }
      .contenu-lien {
        width: 100%;
        height: 100%;
        font-size: 12px;
        color: var(--couleur-primaire);
        border: 1px solid var(--couleur-primaire);
        background: rgba(59, 108, 255, 0.06);
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
        padding: 2px 4px;
      }
      .barre-texte {
        position: absolute;
        top: -34px;
        left: 0;
        display: flex;
        align-items: center;
        gap: 4px;
        padding: 3px 5px;
        background: #fff;
        border: 1px solid var(--couleur-bordure, #d0d0d0);
        border-radius: 6px;
        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
        cursor: default;
        white-space: nowrap;
      }
      .barre-texte .mini {
        border: none;
        background: #f0f0f0;
        border-radius: 4px;
        width: 26px;
        height: 24px;
        cursor: pointer;
        font-size: 12px;
      }
      .barre-texte .mini:hover {
        background: #e2e2e2;
      }
      .barre-texte .champ-taille {
        width: 48px;
        height: 24px;
        text-align: center;
        border: 1px solid var(--couleur-bordure, #d0d0d0);
        border-radius: 4px;
        font-size: 13px;
      }
      .poignee-suppr {
        position: absolute;
        top: -12px;
        right: -12px;
        width: 22px;
        height: 22px;
        border-radius: 50%;
        border: none;
        background: var(--couleur-danger);
        color: #fff;
        cursor: pointer;
        line-height: 1;
        font-size: 15px;
      }
      .poignee-resize {
        position: absolute;
        bottom: -6px;
        right: -6px;
        width: 12px;
        height: 12px;
        background: var(--couleur-primaire);
        border: 2px solid #fff;
        border-radius: 2px;
        cursor: nwse-resize;
      }
    `,
  ],
})
export class PageLayerComponent implements AfterViewInit, OnChanges {
  @ViewChild('canvas') canvasRef!: ElementRef<HTMLCanvasElement>;
  @ViewChild('conteneur') conteneurRef!: ElementRef<HTMLDivElement>;

  /** Document pdf.js partagé. */
  @Input({ required: true }) pdf!: PDFDocumentProxy;
  /** Index de page 0-based. */
  @Input({ required: true }) pageIndex!: number;
  /** Niveau de zoom (1 = 100%). */
  @Input({ required: true }) zoom = 1;
  /** Outil actif sélectionné dans la barre d'outils. */
  @Input({ required: true }) outil: Outil = 'selection';
  /** Toutes les annotations du document. */
  @Input({ required: true }) annotations: Annotation[] = [];
  /** Couleur courante de la barre d'outils. */
  @Input() couleur = '#000000';
  /** Data URL d'une image/signature à poser au prochain clic (image/signature). */
  @Input() chargeUtile: string | null = null;

  @Output() annotationsChange = new EventEmitter<Annotation[]>();
  /** Demande au parent de réinitialiser l'outil (revenir en sélection) après pose. */
  @Output() outilConsomme = new EventEmitter<void>();

  private readonly pdfService = inject(PdfService);

  largeur = 0;
  hauteur = 0;
  dbg = 'v14 (en attente)';
  /** Page pdf.js et ses dimensions de base (échelle 1), mises en cache. */
  private pageProxy?: PDFPageProxy;
  private baseLargeur = 0;
  private baseHauteur = 0;
  /** Rendu pdf.js en cours (pour pouvoir l'annuler au changement de zoom). */
  private renderTask?: ReturnType<PDFPageProxy['render']>;
  selectionId: string | null = null;
  /** Annotation texte actuellement en cours d'édition (double-clic). */
  editionId: string | null = null;

  // État d'interaction en cours.
  private mode: 'aucun' | 'creation' | 'deplacement' | 'redim' = 'aucun';
  private brouillon: Annotation | null = null;
  private depart = { x: 0, y: 0, ax: 0, ay: 0, aw: 0, ah: 0 };

  // --- Helpers de typage pour le template --------------------------------
  texte = (a: Annotation) => a as AnnotationTexte;
  image = (a: Annotation) => a as AnnotationImage;
  forme = (a: Annotation) => a as AnnotationForme;
  lien = (a: Annotation) => a as AnnotationLien;

  /** Épaisseur du trait en pixels écran (points * zoom). */
  strokeW = (a: Annotation) => this.forme(a).epaisseur * this.zoom;

  /**
   * Géométrie d'une forme remplie (rectangle/cercle) en RENTRANT le trait à
   * l'intérieur de la boîte (façon box-sizing: border-box). Sans cet inset, la
   * moitié de l'épaisseur déborderait de la boîte (overflow:visible), ce qui
   * décale visiblement l'anchor sur une petite forme.
   */
  rectInset = (a: Annotation) => {
    const sw = this.strokeW(a);
    return {
      x: sw / 2,
      y: sw / 2,
      width: Math.max(0, a.w * this.largeur - sw),
      height: Math.max(0, a.h * this.hauteur - sw),
    };
  };

  ellipseInset = (a: Annotation) => {
    const sw = this.strokeW(a);
    const rx = (a.w * this.largeur) / 2;
    const ry = (a.h * this.hauteur) / 2;
    return {
      cx: rx,
      cy: ry,
      rx: Math.max(0, rx - sw / 2),
      ry: Math.max(0, ry - sw / 2),
    };
  };

  annotationsPage(): Annotation[] {
    return this.annotations.filter((a) => a.page === this.pageIndex);
  }

  async ngAfterViewInit(): Promise<void> {
    const r = await this.pdfService.chargerPage(this.pdf, this.pageIndex + 1);
    this.pageProxy = r.page;
    this.baseLargeur = r.baseLargeur;
    this.baseHauteur = r.baseHauteur;
    this.redimensionner();
    await this.repeindre();
  }

  async ngOnChanges(changes: SimpleChanges): Promise<void> {
    // Au changement de zoom : on redimensionne la page de façon SYNCHRONE (donc
    // immédiate), puis on repeint. La page est toujours à la bonne taille quand
    // l'utilisateur dessine, même si le rendu pixel est lent (PDF lourd).
    if (changes['zoom'] && this.pageProxy) {
      this.redimensionner();
      await this.repeindre();
    }
  }

  /** Dimensionnement synchrone du canvas + de la couche d'annotations. */
  private redimensionner(): void {
    const dims = this.pdfService.dimensionner(
      this.canvasRef.nativeElement,
      this.baseLargeur,
      this.baseHauteur,
      this.zoom,
    );
    this.largeur = dims.largeur;
    this.hauteur = dims.hauteur;
  }

  /** DEBUG TEMP : mesure repère vert (canvas) vs repère bleu (DOM). */
  private mesurerDebug(): void {
    if (this.pageIndex !== 0) {
      return;
    }
    const canvas = this.canvasRef.nativeElement;
    const cr = canvas.getBoundingClientRect();
    const blue = this.conteneurRef.nativeElement.querySelector(
      '.couche > div',
    ) as HTMLElement | null;
    const br = blue?.getBoundingClientRect();
    const ctx = canvas.getContext('2d');
    let gx = -1,
      gy = -1;
    if (ctx) {
      const w = canvas.width,
        h = canvas.height;
      const data = ctx.getImageData(0, 0, w, h).data;
      for (let y = 0; y < h && gy < 0; y += 1) {
        for (let x = 0; x < w; x += 1) {
          const i = (y * w + x) * 4;
          if (data[i] < 80 && data[i + 1] > 150 && data[i + 2] < 80) {
            gx = x;
            gy = y;
            break;
          }
        }
      }
    }
    const ech = cr.width / canvas.width; // bitmap -> CSS px
    const gL = cr.left + gx * ech;
    const gT = cr.top + gy * ech;
    this.dbg =
      `v16 dpr=${window.devicePixelRatio} zoom=${this.zoom} bmp=${canvas.width}x${canvas.height}\n` +
      `canvas L=${cr.left.toFixed(1)} T=${cr.top.toFixed(1)} W=${cr.width.toFixed(1)}\n` +
      `VERT bmp x=${gx} y=${gy} -> ecran L=${gL.toFixed(1)} T=${gT.toFixed(1)}\n` +
      `BLEU ecran L=${br?.left.toFixed(1)} T=${br?.top.toFixed(1)}\n` +
      `=> ECART vert-bleu X=${br ? (gL - br.left).toFixed(1) : '?'} Y=${br ? (gT - br.top).toFixed(1) : '?'}`;
  }

  /** Rendu pixel (asynchrone) de la page dans le canvas déjà dimensionné. */
  private async repeindre(): Promise<void> {
    if (!this.pageProxy) {
      return;
    }
    // Annule un éventuel rendu en cours (évite les rendus concurrents au zoom
    // rapide, qui peignaient un contenu décalé par rapport au canvas final).
    if (this.renderTask) {
      try {
        this.renderTask.cancel();
      } catch {
        /* déjà terminé */
      }
    }
    const canvas = this.canvasRef.nativeElement;
    this.renderTask = this.pdfService.peindre(this.pageProxy, canvas, this.zoom);
    try {
      await this.renderTask.promise;
    } catch {
      return; // rendu annulé : on laisse le rendu suivant peindre
    }

    // DEBUG TEMP : repère VERT plein (contenu canvas) à la fraction 30%-70%.
    const ctx = canvas.getContext('2d');
    if (ctx) {
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.strokeStyle = 'rgb(0,200,0)';
      ctx.lineWidth = 3;
      ctx.strokeRect(
        0.3 * canvas.width,
        0.3 * canvas.height,
        0.4 * canvas.width,
        0.4 * canvas.height,
      );
    }
    this.mesurerDebug();
  }

  // --- Création d'une annotation par cliquer-glisser ---------------------

  debutInteraction(ev: MouseEvent): void {
    // Clic sur le fond : désélection + sortie d'édition + éventuelle création.
    this.selectionId = null;
    this.editionId = null;

    if (this.outil === 'selection') {
      return;
    }

    // On convertit la position souris en fraction [0..1] à partir de la taille
    // RÉELLE du conteneur (getBoundingClientRect), et non de this.largeur/
    // this.hauteur : ces variables sont mises à jour par rendre() qui est async,
    // elles peuvent donc être périmées pendant un zoom et provoquer un décalage
    // proportionnel au zoom. rect.width/rect.height reflètent toujours l'état
    // affiché à l'écran.
    const rect = this.conteneurRef.nativeElement.getBoundingClientRect();
    const x = (ev.clientX - rect.left) / rect.width;
    const y = (ev.clientY - rect.top) / rect.height;

    // Outils « ponctuels » : pose immédiate puis retour en sélection.
    if (this.outil === 'texte') {
      const t = this.creerTexte(x, y);
      this.poser(t);
      this.editionId = t.id; // édition immédiate du nouveau texte
      return;
    }
    if (this.outil === 'image' || this.outil === 'signature') {
      if (this.chargeUtile) {
        this.creerImage(x, y, this.outil, this.chargeUtile).then((a) =>
          this.poser(a),
        );
      }
      return;
    }
    if (this.outil === 'lien') {
      const url = prompt('URL du lien (https://…)');
      if (url) {
        this.poser(this.creerLien(x, y, url));
      }
      return;
    }

    // Formes : création par glissement.
    this.mode = 'creation';
    this.brouillon = this.creerForme(this.outil, x, y, this.couleur);
    this.depart = { x, y, ax: x, ay: y, aw: 0, ah: 0 };
    this.poser(this.brouillon, false);
    this.attacherSuivi();
  }

  debutDeplacement(ev: MouseEvent, a: Annotation): void {
    ev.stopPropagation();
    this.selectionId = a.id;
    if (this.outil !== 'selection') {
      return;
    }
    this.mode = 'deplacement';
    this.brouillon = a;
    const rect = this.conteneurRef.nativeElement.getBoundingClientRect();
    this.depart = {
      x: (ev.clientX - rect.left) / rect.width,
      y: (ev.clientY - rect.top) / rect.height,
      ax: a.x,
      ay: a.y,
      aw: a.w,
      ah: a.h,
    };
    this.attacherSuivi();
  }

  debutRedim(ev: MouseEvent, a: Annotation): void {
    ev.stopPropagation();
    this.mode = 'redim';
    this.brouillon = a;
    const rect = this.conteneurRef.nativeElement.getBoundingClientRect();
    this.depart = {
      x: (ev.clientX - rect.left) / rect.width,
      y: (ev.clientY - rect.top) / rect.height,
      ax: a.x,
      ay: a.y,
      aw: a.w,
      ah: a.h,
    };
    this.attacherSuivi();
  }

  private attacherSuivi(): void {
    const onMove = (ev: MouseEvent) => this.surMouvement(ev);
    const onUp = () => {
      this.mode = 'aucun';
      this.brouillon = null;
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
      this.emettre();
    };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
  }

  private surMouvement(ev: MouseEvent): void {
    if (this.mode === 'aucun' || !this.brouillon) {
      return;
    }
    const rect = this.conteneurRef.nativeElement.getBoundingClientRect();
    const x = (ev.clientX - rect.left) / rect.width;
    const y = (ev.clientY - rect.top) / rect.height;
    const dx = x - this.depart.x;
    const dy = y - this.depart.y;

    if (this.mode === 'creation') {
      this.brouillon.x = Math.min(this.depart.ax, x);
      this.brouillon.y = Math.min(this.depart.ay, y);
      this.brouillon.w = Math.abs(x - this.depart.ax);
      this.brouillon.h = Math.abs(y - this.depart.ay);
    } else if (this.mode === 'deplacement') {
      this.brouillon.x = this.depart.ax + dx;
      this.brouillon.y = this.depart.ay + dy;
    } else if (this.mode === 'redim') {
      this.brouillon.w = Math.max(0.01, this.depart.aw + dx);
      this.brouillon.h = Math.max(0.01, this.depart.ah + dy);
    }
    // Mutation in place : on déclenche un nouveau tableau pour la détection.
    this.annotations = [...this.annotations];
  }

  // --- Mise à jour / suppression -----------------------------------------

  /** Double-clic sur un texte : passe en mode édition et place le curseur. */
  entrerEdition(a: Annotation, ev: MouseEvent): void {
    ev.stopPropagation();
    this.selectionId = a.id;
    this.editionId = a.id;
    const el = ev.target as HTMLElement;
    // Le focus doit attendre que l'attribut contenteditable soit appliqué.
    setTimeout(() => {
      el.focus();
      const range = document.createRange();
      range.selectNodeContents(el);
      range.collapse(false); // curseur en fin de texte
      const sel = window.getSelection();
      sel?.removeAllRanges();
      sel?.addRange(range);
    });
  }

  /** Fin d'édition (blur) : on enregistre le texte saisi. */
  finEdition(a: Annotation, ev: Event): void {
    const el = ev.target as HTMLElement;
    (a as AnnotationTexte).texte = el.innerText;
    this.editionId = null;
    this.emettre();
  }

  /** Ajuste la taille de police d'un texte par incrément (boutons A− / A+). */
  changerTaille(a: Annotation, delta: number): void {
    this.appliquerTaille(a, (a as AnnotationTexte).taillePolice + delta);
  }

  /** Définit la taille de police d'un texte depuis le champ numérique. */
  majTaille(a: Annotation, valeur: string): void {
    const n = parseInt(valeur, 10);
    if (!Number.isNaN(n)) {
      this.appliquerTaille(a, n);
    }
  }

  private appliquerTaille(a: Annotation, taille: number): void {
    (a as AnnotationTexte).taillePolice = Math.max(6, Math.min(300, taille));
    this.annotations = [...this.annotations];
    this.emettre();
  }

  supprimer(a: Annotation): void {
    this.annotations = this.annotations.filter((x) => x.id !== a.id);
    this.selectionId = null;
    this.emettre();
  }

  // --- Fabriques d'annotations -------------------------------------------

  private poser(a: Annotation, consomme = true): void {
    this.annotations = [...this.annotations, a];
    this.selectionId = a.id;
    if (consomme) {
      this.emettre();
      this.outilConsomme.emit();
    }
  }

  private emettre(): void {
    this.annotationsChange.emit([...this.annotations]);
  }

  private creerTexte(x: number, y: number): AnnotationTexte {
    return {
      id: nouvelId(),
      type: 'texte',
      page: this.pageIndex,
      x,
      y,
      w: 0.25,
      h: 0.05,
      texte: 'Votre texte',
      taillePolice: 16,
      couleur: this.couleur,
    };
  }

  private async creerImage(
    x: number,
    y: number,
    type: 'image' | 'signature',
    dataUrl: string,
  ): Promise<AnnotationImage> {
    const w = type === 'signature' ? 0.3 : 0.3;
    let h = type === 'signature' ? 0.1 : 0.2;
    try {
      const dim = await this.dimensionsImage(dataUrl);
      // Préserve le ratio visuel : (w*largeur)/(h*hauteur) = dim.w/dim.h
      h = (w * this.largeur * dim.h) / (dim.w * this.hauteur);
    } catch {
      // En cas d'échec de lecture, on garde les dimensions par défaut.
    }
    return {
      id: nouvelId(),
      type,
      page: this.pageIndex,
      x,
      y,
      w,
      h,
      dataUrl,
    };
  }

  /** Lit les dimensions naturelles (px) d'une image encodée en data URL. */
  private dimensionsImage(dataUrl: string): Promise<{ w: number; h: number }> {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve({ w: img.naturalWidth, h: img.naturalHeight });
      img.onerror = reject;
      img.src = dataUrl;
    });
  }

  private creerLien(x: number, y: number, url: string): AnnotationLien {
    return {
      id: nouvelId(),
      type: 'lien',
      page: this.pageIndex,
      x,
      y,
      w: 0.2,
      h: 0.04,
      url,
    };
  }

  private creerForme(
    type: Outil,
    x: number,
    y: number,
    couleur: string,
  ): AnnotationForme {
    return {
      id: nouvelId(),
      type: type as AnnotationForme['type'],
      page: this.pageIndex,
      x,
      y,
      w: 0,
      h: 0,
      couleur,
      epaisseur: 2,
      remplissage: null,
    };
  }
}
