// PDF in-book search: text extraction with item geometry, and a matcher
// that maps hits back to page-fraction rects for the overlay renderer.
//
// pdf.js text items carry positional data (transform/width/height in PDF
// units at scale 1) but their strings often omit inter-word spaces — gaps
// are positional. Pages are therefore joined gap-aware, with a char→item
// map so each match can be covered by the rects of the items it spans.

export interface PdfSearchItem {
  /** page-fraction rect of this text item */
  x: number;
  y: number;
  w: number;
  h: number;
  /** char offset of this item's text within the page string */
  start: number;
  length: number;
}

export interface PdfPageText {
  index: number;
  /** normalized, gap-joined page text (lowercased by the matcher) */
  text: string;
  items: PdfSearchItem[];
}

export interface PdfSearchHit {
  page: number;
  rects: number[][];
  pre: string;
  match: string;
  post: string;
}

interface RawItem {
  str: string;
  transform: number[];
  width: number;
  height: number;
  hasEOL: boolean;
}

/** Join one page's text items into a searchable string + item map. */
export function buildPageText(
  rawItems: RawItem[],
  viewportWidth: number,
  viewportHeight: number,
  index: number,
): PdfPageText {
  const vw = viewportWidth || 1;
  const vh = viewportHeight || 1;
  let text = "";
  const items: PdfSearchItem[] = [];

  let prevRight: number | null = null;
  let prevBaseline: number | null = null;

  for (const item of rawItems) {
    if (!item.str) continue;
    const t = item.transform ?? [1, 0, 0, 1, 0, 0];
    const baseline = t[5] ?? 0;
    const x = t[4] ?? 0;
    const size =
      Math.abs(item.height) || Math.abs(t[3]) || Math.abs(t[0]) || 10;
    const w = Math.abs(item.width) || 0;
    const h = size;

    let sep = "";
    if (text && !text.endsWith(" ") && prevRight != null) {
      const newLine =
        item.hasEOL ||
        prevBaseline == null ||
        Math.abs(baseline - prevBaseline) > size * 0.5;
      const gap = x - prevRight;
      if (newLine || gap > size * 0.2) sep = " ";
    }

    const s = item.str.replace(/\s+/g, " ");
    const start = text.length + sep.length;
    text += sep + s;

    items.push({
      x: x / vw,
      y: (vh - baseline - h) / vh,
      w: w / vw,
      h: h / vh,
      start,
      length: s.length,
    });

    prevRight = x + w;
    prevBaseline = baseline;
  }

  return { index, text: text.trimStart(), items };
}

/** Extract all pages of a PDF via pdf.js, reporting progress 0..1. */
export async function extractPdfPages(
  pdf: any,
  onProgress?: (fraction: number) => void,
): Promise<PdfPageText[]> {
  const pages: PdfPageText[] = [];
  const num = pdf.numPages as number;
  for (let i = 0; i < num; i++) {
    const page = await pdf.getPage(i + 1);
    const viewport = page.getViewport({ scale: 1 });
    const tc = await page.getTextContent();
    pages.push(
      buildPageText(tc.items, viewport.width, viewport.height, i),
    );
    onProgress?.((i + 1) / num);
  }
  return pages;
}

const CONTEXT = 60;

/**
 * Case-insensitive search over extracted pages. Returns hits grouped in
 * page order; each hit carries the page-fraction rects of the items it
 * spans (capped to keep pathological fills cheap) plus a trimmed excerpt.
 */
export function searchPdfPages(
  pages: PdfPageText[],
  query: string,
  locales = "en",
): PdfSearchHit[] {
  const needle = query.toLocaleLowerCase(locales).replace(/\s+/g, " ").trim();
  if (!needle) return [];
  const hits: PdfSearchHit[] = [];

  for (const page of pages) {
    const haystack = page.text.toLocaleLowerCase(locales);
    let from = 0;
    for (;;) {
      const s = haystack.indexOf(needle, from);
      if (s === -1) break;
      const e = s + needle.length;
      from = s + Math.max(1, needle.length);

      const rects: number[][] = [];
      for (const it of page.items) {
        if (it.length <= 0) continue;
        if (it.start + it.length <= s || it.start >= e) continue;
        if (rects.length >= 12) break;
        rects.push([it.x, it.y, it.w, it.h]);
      }
      if (!rects.length) continue;

      const pre = page.text.slice(Math.max(0, s - CONTEXT), s);
      const post = page.text.slice(e, e + CONTEXT);
      hits.push({
        page: page.index,
        rects,
        pre: (s > CONTEXT ? "…" : "") + pre.trimStart(),
        match: page.text.slice(s, e),
        post: post.trimEnd() + (page.text.length > e + CONTEXT ? "…" : ""),
      });
    }
  }
  return hits;
}
