/**
 * viewer.ts — Performance-focused PDF viewer with enhanced rendering system
 * 
 * Features:
 * - Double-buffered rendering for flicker-free operation
 * - Adaptive quality based on performance monitoring
 * - Intelligent scroll prediction and pre-loading
 * - Smooth zoom animations with quality transitions
 * - Memory-efficient caching with LRU eviction
 * - Comprehensive performance monitoring
 */

// @ts-ignore - Importing JavaScript modules
import { ViewportTracker }  from "./viewport-tracker.js";
// @ts-ignore - Importing JavaScript modules
import { EnhancedPageManager } from "./page-manager-enhanced.js";
// @ts-ignore - Importing JavaScript modules
import { RenderSystem }      from "./render-system.js";
// @ts-ignore - Importing JavaScript modules
import { MessageBridge }    from "./message-bridge.js";
// @ts-ignore - Importing JavaScript modules
import { AnnotationLayer }  from "./annotation-layer.js";
import { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';

// Type definitions for external modules
type ViewportTrackerType = {
  new (container: HTMLElement, pageWrappers: HTMLElement[], callback: (bufferSet: Set<number>, visibleSet: Set<number>) => void): {
    visibleSet: Set<number>;
    disconnect(): void;
  };
};

type MessageBridgeType = {
  new (nextPage: () => void, prevPage: () => void, handlers: { onAnnotationsSet?: (annotations: any) => void }): {
    postAnnotationCreate(refId: string, pageNum: number, quads: any[], text: string): void;
    postAnnotationClick(refId: string, annotationId: string): void;
    requestAnnotations(refId: string): void;
    postViewerState(refId: string, scale: number, scrollTop: number): void;
    forwardKeydown(ev: KeyboardEvent): void;
    disconnect(): void;
  };
};

type AnnotationLayerType = {
  new (pageWrappers: HTMLElement[], viewports: any[], onClick: (annotationId: string) => void): {
    setAnnotations(annotations: any, scale: number): void;
    onScaleChange(scale: number): void;
  };
};

type EnhancedZoomControllerType = {
  new (container: HTMLElement, pageManager: any, onScaleChange: (newScale: number, bufferSet: Set<number>, visibleSet: Set<number>) => void, getVisibilityState: () => { bufferSet: Set<number>, visibleSet: Set<number> }, zoomLabel: HTMLElement | null): {
    applyScale(scale: number, options: { animate: boolean }): void;
    zoomIn(): void;
    zoomOut(): void;
    getCurrentScale(): number;
    scale: number;
    cleanup(): void;
  };
};

declare global {
  interface Window {
    pdfjsLib: any;
  }
}

// ── DOM refs ─────────────────────────────────────────────────────────────────
const container     = document.getElementById("canvas-container") as HTMLElement | null;
const pagesWrapper  = document.getElementById("pages-wrapper") as HTMLElement | null;
const statusEl      = document.getElementById("status") as HTMLElement | null;
const zoomLabel     = document.getElementById("zoom-label") as HTMLElement | null;
const pageIndicator = document.getElementById("page-indicator") as HTMLElement | null;

// ── Global state ─────────────────────────────────────────────────────────────
const params         = new URLSearchParams(location.search);
const refId          = params.get("ref_id") || "";
const savedZoom      = parseFloat(params.get("zoom") || "");      // NaN if absent
const savedScrollTop = parseFloat(params.get("scroll_top") || ""); // NaN if absent
const DPR            = window.devicePixelRatio || 1;

let pdfDoc: PDFDocumentProxy | null = null;
let pageManager: EnhancedPageManager | null = null;
let viewportTracker: ViewportTracker | null = null;
let zoomController: any | null = null; // Replace with proper type when available
let bridge: MessageBridge | null = null;
let annotationLayer: AnnotationLayer | null = null;
let renderSystem: RenderSystem | null = null;
let viewports: Array<{width: number, height: number}> = [];   // scale=1 dims, used by text layer and annotation layer

// ── Utilities ────────────────────────────────────────────────────────────────
function setStatus(msg: string): void {
  if (statusEl) statusEl.textContent = msg;
}

function showError(msg: string): void {
  const b = document.getElementById("error-banner") as HTMLElement | null;
  if (b) {
    b.textContent   = msg;
    b.style.display = "block";
  }
  setStatus("Error");
}

function refreshPageIndicator(): void {
  if (!pageManager) return;
  
  try {
    const cur = pageManager.getCurrentPage(getCurrentVisibleSet());
    if (pageIndicator) {
      pageIndicator.textContent = cur + " / " + pageManager.numPages;
    }
  } catch (error) {
    console.warn("Failed to refresh page indicator:", error);
    if (pageIndicator) {
      pageIndicator.textContent = "1 / " + (pageManager.numPages || "1");
    }
  }
}

function getCurrentVisibleSet(): Set<number> {
  if (viewportTracker?.visibleSet) {
    return viewportTracker.visibleSet;
  }
  return new Set();
}

async function fitToPage(): Promise<number> {
  if (!pdfDoc) return 1.0;
  const page   = await pdfDoc.getPage(1);
  const vp     = page.getViewport({ scale: 1.0 });
  const scaleW = (container?.clientWidth  || 0 - 40) / vp.width;
  const scaleH = (container?.clientHeight || 0 - 40) / vp.height;
  return Math.max(0.1, Math.min(5.0, Math.min(scaleW, scaleH)));
}

function scrollToPage(pageNum: number): void {
  if (!pageManager) return;
  const wrap = pageManager.pageWrappers[pageNum - 1];
  if (!wrap) return;
  if (container) {
    container.scrollTop += 
      wrap.getBoundingClientRect().top - container.getBoundingClientRect().top;
  }
}

// ── Render request handler ────────────────────────────────────────────────────
async function handleRenderRequest(pageNum: number, scale: number, vpWidth: number, vpHeight: number, quality: number): Promise<void> {
  if (!renderSystem) return;
  
  try {
    await renderSystem.handleRenderRequest(pageNum, scale, vpWidth, vpHeight, quality);
  } catch (error) {
    console.warn(`Failed to render page ${pageNum}:`, error);
  }
}

// ── Text layer rendering ──────────────────────────────────────────────────────
function renderTextLayer(pageNum: number, cssVp: any, textContent: any): void {
  const wrap = pageManager?.pageWrappers[pageNum - 1];
  if (!wrap) return;

  // Remove stale text layer (e.g. after zoom)
  wrap.querySelector(".text-layer")?.remove();

  const div = document.createElement("div");
  div.className    = "text-layer";
  div.style.width  = Math.round(cssVp.width)  + "px";
  div.style.height = Math.round(cssVp.height) + "px";
  wrap.appendChild(div);

  const pdfjsLib = window.pdfjsLib;
  if (!pdfjsLib) return;

  if (typeof pdfjsLib.TextLayer === "function") {
    // PDF.js 3.x+ API
    try {
      const tl = new pdfjsLib.TextLayer({
        textContentSource: textContent,
        container: div,
        viewport: cssVp,
      });
      tl.render().catch(() => {});
    } catch (_) {}
  } else if (typeof pdfjsLib.renderTextLayer === "function") {
    // Legacy API (2.x)
    try {
      pdfjsLib.renderTextLayer({ textContent, container: div, viewport: cssVp });
    } catch (_) {}
  }
}

// ── Text selection → annotation quads ────────────────────────────────────────
function handleTextSelection(): void {
  const sel = window.getSelection();
  if (!sel || sel.isCollapsed || sel.rangeCount === 0) return;

  const selectedText = sel.toString().trim();
  if (!selectedText || !bridge) return;

  const range = sel.getRangeAt(0);

  // Walk up from the anchor node to find the page wrapper
  let node: Node | null = range.startContainer;
  while (node && !(node instanceof HTMLElement && node.classList?.contains("page-wrapper"))) {
    node = node.parentElement;
  }
  if (!node) return;

  const pageNum = parseInt((node as HTMLElement).dataset.page || '1', 10);
  if (!pageNum || pageNum < 1 || pageNum > viewports.length) return;

  const vp0    = viewports[pageNum - 1];
  const scale  = zoomController?.getCurrentScale() || 1;
  const rects  = Array.from(range.getClientRects());
  const wrapR  = (node as HTMLElement).getBoundingClientRect();

  const quads = rects
    .filter(r => r.width > 1 && r.height > 1)
    .map(r => {
      const x0 = (r.left  - wrapR.left) / scale;
      const x1 = (r.right - wrapR.left) / scale;
      const y0 = vp0.height - (r.bottom - wrapR.top) / scale;
      const y1 = vp0.height - (r.top    - wrapR.top) / scale;
      return {
        points: [
          { x: x0, y: y0 },
          { x: x1, y: y0 },
          { x: x1, y: y1 },
          { x: x0, y: y1 },
        ],
      };
    });

  if (quads.length === 0) return;

  // Clear browser selection so the new highlight takes over visually
  sel.removeAllRanges();

  // 0-indexed page number for the backend model
  bridge.postAnnotationCreate(refId, pageNum - 1, quads, selectedText);
}

// ── Main init ────────────────────────────────────────────────────────────────
async function load(): Promise<void> {
  if (!refId) { showError("No ref_id in URL."); return; }
  setStatus("Loading…");

  const pdfjsLib = window.pdfjsLib;
  if (!pdfjsLib) { showError("PDF.js failed to load."); return; }
  pdfjsLib.GlobalWorkerOptions.workerSrc = "brittle://app/pdfjs/build/pdf.worker.min.js";

  const pdfUrl = "brittle://app/pdf?ref_id=" + encodeURIComponent(refId);

  try {
    // 1. Load PDF
    const loadingTask = pdfjsLib.getDocument({ url: pdfUrl });
    pdfDoc = await loadingTask.promise;

    // 2. Fetch all page viewports at scale=1
    setStatus("Reading…");
    viewports = [];
    if (pdfDoc) {
      for (let i = 1; i <= pdfDoc.numPages; i++) {
        const page = await pdfDoc.getPage(i);
        const vp   = page.getViewport({ scale: 1.0 });
        viewports.push({ width: vp.width, height: vp.height });
      }
    }

    // 3. Compute initial scale: use saved zoom if available, else fit full page
    const fittedScale = Math.max(0.1, Math.min(5.0, Math.min(
      ((container?.clientWidth || 0) - 40) / viewports[0].width,
      ((container?.clientHeight || 0) - 40) / viewports[0].height,
    )));
    const initialScale = (savedZoom > 0) ? Math.max(0.1, Math.min(5.0, savedZoom)) : fittedScale;

    // 4. EnhancedPageManager — creates placeholder divs with quality support
    if (!pagesWrapper) throw new Error("Pages wrapper not found");
    pageManager = new EnhancedPageManager(
      pagesWrapper, viewports, initialScale, DPR, 
      (pageNum, scale, vpWidth, vpHeight, gen, quality) => {
        // Handle render request directly
        handleRenderRequest(pageNum, scale, vpWidth, vpHeight, quality);
      }
    );

    // 4.5. RenderSystem — handles actual page rendering
    if (!container) throw new Error("Container not found");
    if (!pdfDoc) throw new Error("PDF document not loaded");
    renderSystem = new RenderSystem(pdfDoc, container, pageManager);

    // 5. ViewportTracker — handles visibility detection
    if (!pageManager) throw new Error("Page manager not initialized");
    viewportTracker = new ViewportTracker(
      container, 
      pageManager.pageWrappers,
      (bufferSet: Set<number>, visibleSet: Set<number>) => {
        pageManager?.reconcile(bufferSet, visibleSet);
        if (renderSystem) renderSystem.setVisibility(visibleSet, bufferSet);
        refreshPageIndicator();
      }
    );

    // 6. EnhancedZoomController — smooth zoom with quality management
    // @ts-ignore - Importing JavaScript module
    const EnhancedZoomController = (await import("./zoom-controller-enhanced.js")).EnhancedZoomController;
    zoomController = new EnhancedZoomController(
      container,
      pageManager,
      (newScale: number, bufferSet: Set<number>, visibleSet: Set<number>) => {
        if (pageManager) pageManager.onScaleChange(newScale, bufferSet, visibleSet);
        if (renderSystem) renderSystem.setScale(newScale);
        annotationLayer?.onScaleChange(newScale);
        sendViewerState();
      },
      () => ({ bufferSet: new Set<number>(), visibleSet: new Set<number>() }),
      zoomLabel
    );
    
    // Set initial scale
    zoomController?.applyScale(initialScale, { animate: false });

    // 7. MessageBridge
    bridge = new MessageBridge(
      () => scrollToPage(Math.min((pageManager?.getCurrentPage(viewportTracker?.visibleSet || new Set<number>()) || 1), pdfDoc?.numPages || 1)),
      () => scrollToPage(Math.max((pageManager?.getCurrentPage(viewportTracker?.visibleSet || new Set<number>()) || 1) - 1, 1)),
      {
        onAnnotationsSet: (annotations: any) => {
          annotationLayer?.setAnnotations(annotations, zoomController?.scale || 1);
        },
      },
    );

    // 8. AnnotationLayer
    annotationLayer = new AnnotationLayer(
      pageManager?.pageWrappers || [],
      viewports,
      (annotationId: string) => bridge?.postAnnotationClick(refId, annotationId),
    );

    // Request annotations from parent
    bridge?.requestAnnotations(refId);

    // 9. Toolbar buttons
    document.getElementById("btn-zoom-out")?.addEventListener("click",
      () => zoomController?.zoomOut());
    document.getElementById("btn-zoom-in")?.addEventListener("click",
      () => zoomController?.zoomIn());
    document.getElementById("btn-zoom-fit")?.addEventListener("click",
      async () => {
        const fitScale = await fitToPage();
        zoomController?.applyScale(fitScale, { animate: false });
      });

    // Keyboard shortcuts + keydown forwarding to parent
    document.addEventListener("keydown", ev => {
      if ((ev.target as HTMLElement).tagName === "INPUT") return;
      if (ev.key === "+" || ev.key === "=") { ev.preventDefault(); zoomController?.zoomIn(); }
      if (ev.key === "-")                   { ev.preventDefault(); zoomController?.zoomOut(); }
      if (ev.key === "0")                   { ev.preventDefault(); fitToPage().then(s => zoomController?.applyScale(s, { animate: false })); }
      bridge?.forwardKeydown(ev);
    });

    // mouseup on the document → check for text selection to create highlights
    document.addEventListener("mouseup", ev => {
      setTimeout(() => handleTextSelection(), 0);
    });

    // Scroll → update page indicator + debounced state save
    let _scrollSaveTimer: number | null = null;
    container?.addEventListener("scroll", () => {
      refreshPageIndicator();
      if (_scrollSaveTimer) clearTimeout(_scrollSaveTimer);
      _scrollSaveTimer = window.setTimeout(sendViewerState, 500) as unknown as number;
    }, { passive: true });

    // Restore saved scroll position (rAF ensures layout is ready)
    if (savedScrollTop > 0 && container) {
      requestAnimationFrame(() => { container.scrollTop = savedScrollTop; });
    }

    // Lifecycle: tab hidden → free caches; tab visible → re-reconcile
    document.addEventListener("visibilitychange", () => {
      if (document.hidden) {
        pdfDoc?.destroy();
      } else {
        // Trigger a visibility update to refresh pages
        const bufferSet = new Set<number>();
        const visibleSet = new Set<number>();
        pageManager?.reconcile(bufferSet, visibleSet);
      }
    });

    // Cleanup on unload
    window.addEventListener("beforeunload", () => {
      viewportTracker?.disconnect();
      bridge?.disconnect();
      renderSystem?.cleanup();
      pdfDoc?.destroy();
      if (zoomController) zoomController.cleanup();
    });

    // DPR change (e.g., moving window to a monitor with different DPI)
    if (window.matchMedia) {
      window.matchMedia(`(resolution: ${DPR}dppx)`).addEventListener("change", () => {
        if (pageManager && zoomController) {
          const bufferSet = new Set<number>();
          const visibleSet = new Set<number>();
          pageManager?.onScaleChange(zoomController.getCurrentScale(), bufferSet, visibleSet);
        }
      });
    }

    if (pageIndicator && pdfDoc) {
      pageIndicator.textContent = "1 / " + pdfDoc.numPages;
    }
    setStatus("Ready");

  } catch (e) {
    showError("Could not load PDF: " + (e instanceof Error ? e.message : String(e)));
  }
}

function sendViewerState(): void {
  if (!bridge || !zoomController || !container) return;
  bridge.postViewerState(refId, zoomController.getCurrentScale(), container.scrollTop);
}

// Start the viewer
load();