// app-pdf.jsx — PdfViewer: single-page canvas viewer for the board packet.
// Exposes an imperative goToPage(n) via ref so the outline can drive it.
(function () {
  const { useRef, useEffect, useState, forwardRef, useImperativeHandle } = React;

  const PdfViewer = forwardRef(function PdfViewer({ url }, ref) {
    const scrollRef = useRef(null);
    const canvasRef = useRef(null);
    const textLayerRef = useRef(null);
    const docRef = useRef(null);
    const renderTaskRef = useRef(null);
    const textLayerTaskRef = useRef(null);
    const [numPages, setNumPages] = useState(0);
    const [pageNum, setPageNum] = useState(1);
    const [zoom, setZoom] = useState(1);
    const [rotation, setRotation] = useState(0); // 0 | 90 | 180 | 270 — added on top of the page's intrinsic rotation
    const [cw, setCw] = useState(0);
    const [status, setStatus] = useState("loading"); // loading | ready | error

    useImperativeHandle(ref, () => ({
      goToPage(n) {
        const total = docRef.current ? docRef.current.numPages : null;
        if (!n || isNaN(n)) return;
        let np = Math.max(1, Math.floor(n));
        if (total) np = Math.min(np, total);
        setPageNum(np);
        if (scrollRef.current) scrollRef.current.scrollTo({ top: 0 });
      },
    }), []);

    // ---- load the document (once per url) ----
    useEffect(() => {
      let cancelled = false;
      setStatus("loading");
      setNumPages(0);
      const task = window.pdfjsLib.getDocument(url);
      task.promise.then((pdf) => {
        if (cancelled) { try { pdf.destroy(); } catch (e) {} return; }
        docRef.current = pdf;
        setNumPages(pdf.numPages);
        setPageNum(1);
        setStatus("ready");
      }).catch(() => { if (!cancelled) setStatus("error"); });
      return () => {
        cancelled = true;
        if (renderTaskRef.current) { try { renderTaskRef.current.cancel(); } catch (e) {} }
        if (textLayerTaskRef.current) { try { textLayerTaskRef.current.cancel(); } catch (e) {} }
        if (docRef.current) { try { docRef.current.destroy(); } catch (e) {} docRef.current = null; }
      };
    }, [url]);

    // ---- track container width for fit-to-width ----
    useEffect(() => {
      const el = scrollRef.current;
      if (!el || typeof ResizeObserver === "undefined") return;
      const ro = new ResizeObserver((entries) => {
        for (const e of entries) {
          const w = e.contentRect.width;
          if (w > 0) setCw(w);
        }
      });
      ro.observe(el);
      return () => ro.disconnect();
    }, []);

    // ---- stabilise text selection ----
    // Toggle a .selecting class on the text layer for the duration of a drag so
    // the .endOfContent boundary expands; this keeps the highlight on the row
    // under the cursor instead of leaping to following lines when the pointer
    // enters the empty space after a line.
    useEffect(() => {
      const div = textLayerRef.current;
      if (!div) return;
      const start = () => div.classList.add("selecting");
      const stop = () => div.classList.remove("selecting");
      div.addEventListener("pointerdown", start);
      window.addEventListener("pointerup", stop);
      window.addEventListener("pointercancel", stop);
      return () => {
        div.removeEventListener("pointerdown", start);
        window.removeEventListener("pointerup", stop);
        window.removeEventListener("pointercancel", stop);
      };
    }, []);

    // ---- render the current page ----
    useEffect(() => {
      const pdf = docRef.current;
      if (status !== "ready" || !pdf || !cw || cw <= 0) return;
      let cancelled = false;
      (async () => {
        try {
          if (renderTaskRef.current) { try { renderTaskRef.current.cancel(); } catch (e) {} }
          if (textLayerTaskRef.current) { try { textLayerTaskRef.current.cancel(); } catch (e) {} }
          const page = await pdf.getPage(pageNum);
          if (cancelled) return;
          const base = page.getViewport({ scale: 1, rotation });
          const fit = (cw - 28) / base.width;
          const scale = Math.max(0.2, fit * zoom);
          const viewport = page.getViewport({ scale, rotation });
          const canvas = canvasRef.current;
          if (!canvas) return;
          const ctx = canvas.getContext("2d");
          const dpr = window.devicePixelRatio || 1;
          canvas.width = Math.floor(viewport.width * dpr);
          canvas.height = Math.floor(viewport.height * dpr);
          canvas.style.width = Math.floor(viewport.width) + "px";
          canvas.style.height = Math.floor(viewport.height) + "px";
          const transform = dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : undefined;
          const rt = page.render({ canvasContext: ctx, viewport, transform });
          renderTaskRef.current = rt;
          await rt.promise;

          // ---- selectable text layer (overlay of real, transparent text) ----
          const textLayer = textLayerRef.current;
          if (textLayer && window.pdfjsLib.renderTextLayer) {
            textLayer.innerHTML = "";
            textLayer.style.width = Math.floor(viewport.width) + "px";
            textLayer.style.height = Math.floor(viewport.height) + "px";
            textLayer.style.setProperty("--scale-factor", String(scale));
            const textContent = await page.getTextContent();
            if (cancelled) return;
            const tlTask = window.pdfjsLib.renderTextLayer({
              textContent,
              container: textLayer,
              viewport,
              textDivs: [],
            });
            textLayerTaskRef.current = tlTask;
            await tlTask.promise;
            if (cancelled) return;
            // Append an "end of content" boundary. While a drag-select is in
            // progress this element (expanded to cover the whole layer via the
            // .selecting class) catches the cursor in the blank space past a
            // line's last word — without it, dragging into that gap makes the
            // browser jump the selection down to later lines.
            const eoc = document.createElement("div");
            eoc.className = "endOfContent";
            textLayer.appendChild(eoc);
          }
        } catch (e) { /* render cancelled — ignore */ }
      })();
      return () => { cancelled = true; };
    }, [status, pageNum, zoom, rotation, cw, numPages]);

    function go(delta) {
      setPageNum((p) => Math.min(Math.max(1, p + delta), numPages || 1));
      if (scrollRef.current) scrollRef.current.scrollTo({ top: 0 });
    }

    return (
      <div className="pdfv">
        <div className="pdfv__toolbar">
          <div className="pdfv__group">
            <button className="pdfv__btn" onClick={() => go(-1)} disabled={pageNum <= 1} title="Previous page">‹</button>
            <span className="pdfv__pageind">
              <span className="pdfv__pagelabel">Page</span>
              <input
                className="pdfv__pageinput"
                value={pageNum}
                inputMode="numeric"
                aria-label="Go to page"
                title="Type a page number and press Enter to jump there"
                onFocus={(e) => e.target.select()}
                onChange={(e) => {
                  const v = parseInt(e.target.value.replace(/[^0-9]/g, ""), 10);
                  if (!isNaN(v)) setPageNum(Math.min(Math.max(1, v), numPages || 1));
                }}
                onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); }}
              />
              <span className="pdfv__pagetotal">/ {numPages || "–"}</span>
            </span>
            <button className="pdfv__btn" onClick={() => go(1)} disabled={pageNum >= numPages} title="Next page">›</button>
          </div>
          <div className="pdfv__group">
            <button className="pdfv__btn" onClick={() => setRotation((r) => (r + 90) % 360)} title="Rotate page 90° clockwise" aria-label="Rotate page clockwise">↻</button>
            <button className="pdfv__btn" onClick={() => setZoom((z) => Math.max(0.5, +(z - 0.15).toFixed(2)))} title="Zoom out">−</button>
            <button className="pdfv__btn pdfv__btn--txt" onClick={() => setZoom(1)} title="Fit to width">{Math.round(zoom * 100)}%</button>
            <button className="pdfv__btn" onClick={() => setZoom((z) => Math.min(3, +(z + 0.15).toFixed(2)))} title="Zoom in">+</button>
          </div>
        </div>
        <div className="pdfv__scroll" ref={scrollRef}>
          {status === "loading" && <div className="pdfv__msg"><span className="spinner"></span>Loading packet…</div>}
          {status === "error" && <div className="pdfv__msg pdfv__msg--err">Couldn’t open this PDF. Try re-uploading the board packet.</div>}
          <div className="pdfv__page" style={{ display: status === "ready" ? "block" : "none" }}>
            <canvas ref={canvasRef} className="pdfv__canvas" />
            <div ref={textLayerRef} className="textLayer" aria-hidden="true"></div>
          </div>
        </div>
      </div>
    );
  });

  window.PdfViewer = PdfViewer;
})();
