/**
 * Main-thread client for the mupdf native-editing worker.
 *
 * The worker caches one open PDFDocument. To avoid shipping the whole (up to
 * ~10MB) PDF across the wire on every call, we track the *reference identity*
 * of the bytes we last sent: every edit produces a brand-new Uint8Array, so
 * `bytes !== lastSentBytes` is an exact "the document changed" signal. When the
 * worker performs an edit, its in-memory doc already equals the returned bytes,
 * so we adopt that array as `lastSentBytes` and skip re-parsing next call.
 *
 * All ops are serialized through a promise chain so the bytes-sync stays
 * deterministic even if callers fire several queries at once.
 */
import type {
  MupdfRequest,
  MupdfResponse,
  MupdfOp,
  PageContent,
  RedactMark,
  RedactImageMode,
  TextEditParams,
  ImageEditParams,
  Rect4,
} from './mupdf-types';

let worker: Worker | null = null;
let seq = 0;
let gen = 0;
let lastSentBytes: Uint8Array | null = null;
let chain: Promise<unknown> = Promise.resolve();
const pending = new Map<number, (r: MupdfResponse) => void>();

function getWorker(): Worker {
  if (!worker) {
    worker = new Worker(new URL('../workers/mupdf.worker.ts', import.meta.url), { type: 'module' });
    worker.onmessage = (e: MessageEvent<MupdfResponse>) => {
      const r = e.data;
      // id < 0 is a fatal worker-level failure (e.g. wasm failed to instantiate):
      // fail every in-flight request instead of letting callers hang forever.
      if (r.id < 0) {
        console.error('[mupdf worker]', r.error);
        for (const cb of pending.values()) cb(r);
        pending.clear();
        return;
      }
      pending.get(r.id)?.(r);
      pending.delete(r.id);
    };
    // worker-level failures (e.g. the module graph failing to load) surface here,
    // NOT via in-worker listeners — fail in-flight requests rather than hang.
    worker.onerror = (e) => {
      const msg = `worker load error: ${e.message || 'unknown'}`;
      console.error('[mupdf]', msg, e.filename ? `(${e.filename}:${e.lineno})` : '');
      for (const cb of pending.values()) cb({ id: -1, ok: false, error: msg });
      pending.clear();
    };
  }
  return worker;
}

/** Free the worker + its ~10MB wasm heap (call when the document is closed). */
export function disposeMupdf() {
  worker?.terminate();
  worker = null;
  lastSentBytes = null;
  pending.clear();
}

function send(bytes: Uint8Array, payload: MupdfOp): Promise<MupdfResponse> {
  const id = ++seq;
  const w = getWorker();
  const needsBytes = bytes !== lastSentBytes;
  const req: MupdfRequest = { id, gen: needsBytes ? ++gen : gen, payload };
  const transfer: Transferable[] = [];
  if (needsBytes) {
    const copy = bytes.slice(); // detach-safe copy; keep caller's array intact
    req.bytes = copy;
    transfer.push(copy.buffer);
    lastSentBytes = bytes; // remember the caller's ref for identity comparison
  }
  return new Promise<MupdfResponse>((resolve, reject) => {
    pending.set(id, (r) => (r.ok ? resolve(r) : reject(new Error(r.error || 'mupdf error'))));
    w.postMessage(req, transfer);
  });
}

/** Serialize ops so lastSentBytes bookkeeping is race-free. */
function run(bytes: Uint8Array, payload: MupdfOp): Promise<MupdfResponse> {
  const p = chain.then(() => send(bytes, payload));
  chain = p.catch(() => {});
  return p;
}

/** After an edit, the worker's in-memory doc already equals `out` — adopt it. */
function adopt(out: Uint8Array): Uint8Array {
  lastSentBytes = out;
  return out;
}

/* ---------- typed operations ---------- */

/** Extract editable text spans + images (with geometry) for one page. */
export async function queryPage(bytes: Uint8Array, pageIndex: number): Promise<PageContent> {
  const r = await run(bytes, { op: 'query-page', pageIndex });
  return r.page!;
}

/** Apply true (content-destructive) redaction to the given rectangles. */
export async function redactAreas(
  bytes: Uint8Array,
  marks: RedactMark[],
  imageMode: RedactImageMode = 'pixels',
): Promise<Uint8Array> {
  const r = await run(bytes, { op: 'redact', marks, imageMode });
  return adopt(r.output!);
}

/** Replace an existing text span's characters in-place (redact + redraw). */
export async function editTextSpan(bytes: Uint8Array, params: TextEditParams): Promise<Uint8Array> {
  const r = await run(bytes, { op: 'edit-text', params });
  return adopt(r.output!);
}

/** Move/scale/rotate/flip an existing image (redact old placement + redraw). */
export async function editImage(bytes: Uint8Array, params: ImageEditParams): Promise<Uint8Array> {
  const r = await run(bytes, { op: 'edit-image', params });
  return adopt(r.output!);
}

/**
 * Flatten native-edit history into clean, export-ready bytes (memory-light, in
 * the worker). Does NOT `adopt()` — the live working `doc.bytes` stays the
 * editing version; these bytes are for the exported file only.
 */
export async function compactPdf(bytes: Uint8Array): Promise<Uint8Array> {
  const r = await run(bytes, { op: 'compact' });
  return r.output!;
}

/** Delete an existing image from a page. */
export async function deleteImage(
  bytes: Uint8Array,
  pageIndex: number,
  imageIndex: number,
  bbox: Rect4,
): Promise<Uint8Array> {
  const r = await run(bytes, { op: 'delete-image', pageIndex, imageIndex, bbox });
  return adopt(r.output!);
}
