/*
 * Copyright 2025 Palantir Technologies, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { useEffect, useState } from "react";
import { OUTLINE_HEADING_SIZE_RATIO, OUTLINE_MAX_HEADING_LENGTH } from "../constants.js";
const EMPTY_OUTLINE = [];
export function usePdfOutline(document) {
  const [outlineItems, setOutlineItems] = useState(EMPTY_OUTLINE);
  useEffect(function () {
    if (document == null) {
      setOutlineItems(EMPTY_OUTLINE);
      return;
    }
    let cancelled = false;
    void (async () => {
      try {
        // Try embedded bookmarks first
        const outline = await document.getOutline();
        if (cancelled) return;
        if (outline != null && outline.length > 0) {
          const items = await resolveBookmarkOutline(document, outline);
          if (!cancelled) {
            setOutlineItems(items);
          }
          return;
        }

        // Fall back to extracting headings from text content
        const items = await extractHeadingsFromText(document);
        if (!cancelled) {
          setOutlineItems(items.length > 0 ? items : EMPTY_OUTLINE);
        }
      } catch {
        // If extraction fails entirely, leave outline empty
        if (!cancelled) {
          setOutlineItems(EMPTY_OUTLINE);
        }
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [document]);
  return outlineItems;
}
async function resolveBookmarkOutline(document, outline) {
  const items = [];
  const resolveItems = async (nodes, depth) => {
    for (const node of nodes) {
      let pageNumber = 1;
      try {
        if (typeof node.dest === "string") {
          const dest = await document.getDestination(node.dest);
          if (dest != null) {
            const pageIndex = await document.getPageIndex(dest[0]);
            pageNumber = pageIndex + 1;
          }
        } else if (Array.isArray(node.dest) && node.dest.length > 0) {
          const pageIndex = await document.getPageIndex(node.dest[0]);
          pageNumber = pageIndex + 1;
        }
      } catch {
        // If destination resolution fails, default to page 1
      }
      items.push({
        title: node.title,
        depth,
        pageNumber,
        bold: node.bold,
        italic: node.italic
      });
      if (node.items.length > 0) {
        await resolveItems(node.items, depth + 1);
      }
    }
  };
  await resolveItems(outline, 0);
  return items;
}
function isTextItem(item) {
  return "str" in item && "transform" in item;
}
function roundFontSize(size) {
  return Math.round(size * 10) / 10;
}

/**
 * Extract the font size from a pdfjs TextItem.
 * Prefer `height` when available, otherwise derive from the transform matrix.
 * The transform is [scaleX, skewY, skewX, scaleY, translateX, translateY].
 * Font size = sqrt(skewX² + scaleY²), which handles rotated text too.
 */
function getFontSize(item) {
  if (item.height > 0) {
    return item.height;
  }
  const transform = item.transform;
  if (Array.isArray(transform) && transform.length >= 4) {
    return Math.sqrt(transform[2] * transform[2] + transform[3] * transform[3]);
  }
  return 0;
}
async function extractHeadingsFromText(document) {
  const numPages = document.numPages;

  // Fetch all pages' text content in parallel
  const pageContents = await Promise.all(Array.from({
    length: numPages
  }, (_, i) => document.getPage(i + 1).then(page => page.getTextContent())));

  // Collect font size frequencies across all pages
  const sizeCounts = new Map();
  for (const textContent of pageContents) {
    for (const rawItem of textContent.items) {
      const item = rawItem;
      if (!isTextItem(item) || item.str.trim().length === 0) {
        continue;
      }
      const fontSize = getFontSize(item);
      if (fontSize > 0) {
        const rounded = roundFontSize(fontSize);
        sizeCounts.set(rounded, (sizeCounts.get(rounded) ?? 0) + 1);
      }
    }
  }

  // Find the most common font size (body text)
  let bodySize = 0;
  let maxCount = 0;
  for (const [size, count] of sizeCounts) {
    if (count > maxCount) {
      maxCount = count;
      bodySize = size;
    }
  }
  if (bodySize === 0) {
    return [];
  }

  // Heading threshold: text significantly larger than body text
  const headingThreshold = bodySize * OUTLINE_HEADING_SIZE_RATIO;

  // Collect all unique heading sizes above threshold
  const headingSizes = new Set();
  for (const [size] of sizeCounts) {
    if (size >= headingThreshold) {
      headingSizes.add(size);
    }
  }
  if (headingSizes.size === 0) {
    return [];
  }

  // Sort heading sizes descending (largest = depth 0)
  const sortedSizes = [...headingSizes].sort((a, b) => b - a);
  const sizeToDepth = new Map();
  for (let i = 0; i < sortedSizes.length; i++) {
    sizeToDepth.set(sortedSizes[i], i);
  }

  // Extract headings with page numbers.
  // pdfjs splits visual lines into multiple TextItems (e.g. "1." and
  // "Introduction" are separate items). We accumulate all items on the
  // same visual line and treat the line as a heading if most of its
  // content is at a heading font size.
  const candidateHeadings = [];
  for (let pageIdx = 0; pageIdx < pageContents.length; pageIdx++) {
    const textContent = pageContents[pageIdx];
    const pageNumber = pageIdx + 1;
    let lineItems = [];
    let linePageNumber = pageNumber;
    let lineFontName = "";
    let lineHeadingFontSize = 0;
    let lastFlushWasHeading = false;
    const flushLine = () => {
      if (lineItems.length === 0) return;

      // Check if the majority of the line's non-whitespace content is heading-sized
      let headingChars = 0;
      let totalChars = 0;
      for (const li of lineItems) {
        const len = li.text.trim().length;
        totalChars += len;
        if (sizeToDepth.has(roundFontSize(li.fontSize))) {
          headingChars += len;
        }
      }
      if (totalChars > 0 && headingChars / totalChars > 0.5) {
        const fullText = lineItems.map(li => li.text).join("").trim();
        if (fullText.length > 0) {
          candidateHeadings.push({
            text: fullText,
            fontSize: lineHeadingFontSize,
            pageNumber: linePageNumber,
            fontName: lineFontName,
            contiguous: lastFlushWasHeading
          });
        }
        lastFlushWasHeading = true;
      } else {
        lastFlushWasHeading = false;
      }
      lineItems = [];
      lineHeadingFontSize = 0;
    };
    for (const rawItem of textContent.items) {
      const item = rawItem;
      if (!isTextItem(item)) {
        continue;
      }
      const fontSize = getFontSize(item);
      const rounded = roundFontSize(fontSize);
      const isHeadingSized = sizeToDepth.has(rounded);
      if (isHeadingSized && lineHeadingFontSize === 0) {
        lineHeadingFontSize = rounded;
        linePageNumber = pageNumber;
        lineFontName = item.fontName;
      }
      if (lineHeadingFontSize > 0) {
        lineItems.push({
          text: item.str,
          fontSize
        });
      }
      if (item.hasEOL) {
        flushLine();
      }
    }

    // Flush remaining items at end of page
    flushLine();
  }

  // Merge consecutive heading lines that were contiguous (no body text between them)
  // and at the same font size. This joins multi-line titles that wrap in the PDF.
  const mergedHeadings = [];
  for (const heading of candidateHeadings) {
    const prev = mergedHeadings[mergedHeadings.length - 1];
    if (prev != null && heading.contiguous && Math.abs(prev.fontSize - heading.fontSize) < 0.1) {
      prev.text += " " + heading.text;
    } else {
      mergedHeadings.push({
        ...heading
      });
    }
  }
  return mergedHeadings.filter(heading => heading.text.trim().length <= OUTLINE_MAX_HEADING_LENGTH).map(heading => ({
    title: heading.text.trim(),
    depth: sizeToDepth.get(heading.fontSize) ?? 0,
    pageNumber: heading.pageNumber,
    bold: false,
    italic: false
  }));
}
//# sourceMappingURL=usePdfOutline.js.map