import * as pdfjs from 'pdfjs-dist';
import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
import { bufferOf } from './util';

pdfjs.GlobalWorkerOptions.workerSrc = workerUrl;

export type { PDFDocumentProxy, PDFPageProxy };

export interface LoadOptions {
  password?: string;
}

/**
 * Parse PDF bytes with pdf.js. Always works on a COPY, because pdf.js transfers
 * (and detaches) the buffer it is handed — we must not clobber the caller's bytes.
 */
export async function loadPdf(bytes: Uint8Array, opts: LoadOptions = {}): Promise<PDFDocumentProxy> {
  const task = pdfjs.getDocument({
    data: new Uint8Array(bufferOf(bytes)),
    password: opts.password,
    useSystemFonts: true,
  });
  return task.promise;
}

/** Thrown when a PDF needs (or was given a wrong) password. */
export const PasswordException = pdfjs.PasswordResponses;

export function isPasswordException(err: unknown): boolean {
  return !!err && typeof err === 'object' && (err as any).name === 'PasswordException';
}

/** Render a page onto a canvas at a given CSS scale (device-pixel aware). */
export async function renderPage(
  page: PDFPageProxy,
  scale: number,
  canvas: HTMLCanvasElement,
  opts: { rotation?: number; dpr?: number } = {},
): Promise<{ width: number; height: number }> {
  const dpr = opts.dpr ?? Math.min(window.devicePixelRatio || 1, 2.5);
  const viewport = page.getViewport({ scale, rotation: opts.rotation });
  const outW = Math.floor(viewport.width);
  const outH = Math.floor(viewport.height);
  canvas.width = Math.floor(outW * dpr);
  canvas.height = Math.floor(outH * dpr);
  canvas.style.width = `${outW}px`;
  canvas.style.height = `${outH}px`;
  const ctx = canvas.getContext('2d', { alpha: false })!;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  await page.render({ canvasContext: ctx, viewport, background: '#ffffff' } as any).promise;
  return { width: outW, height: outH };
}

/** Render a small thumbnail bitmap for the page rail. Returns a data URL. */
export async function renderThumbnail(page: PDFPageProxy, targetW = 130): Promise<string> {
  const base = page.getViewport({ scale: 1 });
  const scale = targetW / base.width;
  const viewport = page.getViewport({ scale });
  const canvas = document.createElement('canvas');
  canvas.width = Math.ceil(viewport.width);
  canvas.height = Math.ceil(viewport.height);
  const ctx = canvas.getContext('2d', { alpha: false })!;
  await page.render({ canvasContext: ctx, viewport, background: '#ffffff' } as any).promise;
  return canvas.toDataURL('image/jpeg', 0.72);
}

/** Render a full page to a Blob (used by PDF→image conversion). */
export async function renderPageToBlob(
  page: PDFPageProxy,
  scale: number,
  type: 'image/png' | 'image/jpeg' | 'image/webp' = 'image/png',
  quality = 0.92,
): Promise<Blob> {
  const viewport = page.getViewport({ scale });
  const canvas = document.createElement('canvas');
  canvas.width = Math.ceil(viewport.width);
  canvas.height = Math.ceil(viewport.height);
  const ctx = canvas.getContext('2d', { alpha: type === 'image/png' })!;
  if (type !== 'image/png') { ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, canvas.width, canvas.height); }
  await page.render({ canvasContext: ctx, viewport, background: '#ffffff' } as any).promise;
  return new Promise((res) => canvas.toBlob((b) => res(b!), type, quality));
}

export interface TextItem {
  str: string;
  x: number;
  y: number;
  width: number;
  height: number;
  fontName: string;
  hasEOL: boolean;
}

/** Extract text items in PDF user-space coordinates (origin bottom-left). */
export async function getPageTextItems(page: PDFPageProxy): Promise<TextItem[]> {
  const content = await page.getTextContent();
  const items: TextItem[] = [];
  for (const it of content.items as any[]) {
    if (typeof it.str !== 'string') continue;
    const t = it.transform; // [a,b,c,d,e,f]
    items.push({
      str: it.str,
      x: t[4],
      y: t[5],
      width: it.width,
      height: it.height || Math.hypot(t[1], t[3]),
      fontName: it.fontName || '',
      hasEOL: !!it.hasEOL,
    });
  }
  return items;
}

/**
 * Reconstruct plain text from a page using simple reading-order heuristics:
 * group items into lines by baseline, sort left→right, then top→bottom.
 */
export async function getPageText(page: PDFPageProxy): Promise<string> {
  const items = await getPageTextItems(page);
  if (!items.length) return '';
  const lines: { y: number; parts: TextItem[] }[] = [];
  const tol = 3;
  for (const it of items) {
    let line = lines.find((l) => Math.abs(l.y - it.y) <= tol);
    if (!line) { line = { y: it.y, parts: [] }; lines.push(line); }
    line.parts.push(it);
  }
  lines.sort((a, b) => b.y - a.y);
  return lines
    .map((l) => {
      l.parts.sort((a, b) => a.x - b.x);
      let s = '';
      let prevRight = null as number | null;
      for (const p of l.parts) {
        if (prevRight != null && p.x - prevRight > p.height * 0.35 && !s.endsWith(' ')) s += ' ';
        s += p.str;
        prevRight = p.x + p.width;
      }
      return s.trimEnd();
    })
    .join('\n');
}

/** Get the document outline/bookmarks (may be null). */
export async function getOutline(doc: PDFDocumentProxy) {
  return doc.getOutline();
}

/** Fields present in an AcroForm (widget rectangles + types), for auto-highlighting. */
export async function getFieldObjects(doc: PDFDocumentProxy) {
  try {
    return await doc.getFieldObjects();
  } catch {
    return null;
  }
}
