// Рендеринг PDF через pdf.js с ленивой подгрузкой страниц и миниатюр.
// Страницы создаются как плейсхолдеры нужного размера; canvas+текстовый слой
// рендерятся только когда страница попадает в зону видимости (IntersectionObserver).
//
// pdf.js НЕ входит в репозиторий — положите сборку в vendor/pdfjs/
// (см. extension/vendor/README.md). Ожидаются файлы pdf.mjs и pdf.worker.mjs.

import * as pdfjsLib from "../../vendor/pdfjs/pdf.mjs";

pdfjsLib.GlobalWorkerOptions.workerSrc = chrome.runtime.getURL("vendor/pdfjs/pdf.worker.mjs");

export class PdfController {
  constructor(pagesEl, containerEl) {
    this.pagesEl = pagesEl;
    this.containerEl = containerEl || pagesEl.parentElement;
    this.pdf = null;
    this.scale = 1.0;
    this.rotation = 0; // 0|90|180|270
    this.pageText = [];
    this.baseViewports = []; // viewport при scale=1 (для размеров плейсхолдеров)
    this.wraps = new Map(); // num -> div.page-wrap
    this.rendered = new Set();
    this.searchRe = null;
    this.onPageChange = null;
    this.onPageRendered = null; // (num, wrap) — после отрисовки страницы
    this._renderObserver = null;
    this._pageObserver = null;
  }

  get numPages() {
    return this.pdf?.numPages || 0;
  }

  /**
   * @param arrayBuffer данные PDF
   * @param onPassword  async (reason) => string|null — запрос пароля у пользователя
   *                    reason: 1 = нужен пароль, 2 = пароль неверный.
   */
  async load(arrayBuffer, onPassword) {
    const task = pdfjsLib.getDocument({ data: arrayBuffer });
    if (onPassword) {
      task.onPassword = (updateCallback, reason) => {
        Promise.resolve(onPassword(reason)).then((pw) => updateCallback(pw || ""));
      };
    }
    this.pdf = await task.promise;
    // Кэшируем базовые размеры всех страниц (дёшево — без рендера).
    for (let n = 1; n <= this.numPages; n++) {
      const page = await this.pdf.getPage(n);
      this.baseViewports[n - 1] = page.getViewport({ scale: 1 });
    }
    return this.numPages;
  }

  // --- Построение плейсхолдеров + ленивый рендер ---------------------------

  /** Размеры плейсхолдера с учётом масштаба и поворота (90/270 меняют стороны). */
  _dims(num) {
    const vp = this.baseViewports[num - 1];
    const swap = this.rotation % 180 !== 0;
    return {
      w: (swap ? vp.height : vp.width) * this.scale,
      h: (swap ? vp.width : vp.height) * this.scale,
    };
  }

  buildPages() {
    this.pagesEl.innerHTML = "";
    this.wraps.clear();
    this.rendered.clear();
    this._disconnectObservers();

    for (let n = 1; n <= this.numPages; n++) {
      const { w, h } = this._dims(n);
      const wrap = document.createElement("div");
      wrap.className = "page-wrap";
      wrap.dataset.page = n;
      wrap.style.width = `${w}px`;
      wrap.style.height = `${h}px`;
      this.pagesEl.appendChild(wrap);
      this.wraps.set(n, wrap);
    }

    // Рендер при приближении к области видимости; выгрузка дальних страниц
    // (скользящее окно ±800px) — экономия памяти на больших документах.
    this._renderObserver = new IntersectionObserver(
      (entries) => {
        for (const e of entries) {
          const n = +e.target.dataset.page;
          if (e.isIntersecting) this._render(n);
          else this._unload(n);
        }
      },
      { root: this.containerEl, rootMargin: "800px 0px" },
    );
    // Текущая страница (для номера/миниатюр).
    this._pageObserver = new IntersectionObserver(
      (entries) => {
        for (const e of entries) {
          if (e.isIntersecting && this.onPageChange) this.onPageChange(+e.target.dataset.page);
        }
      },
      { root: this.containerEl, threshold: 0.5 },
    );
    for (const wrap of this.wraps.values()) {
      this._renderObserver.observe(wrap);
      this._pageObserver.observe(wrap);
    }
  }

  async _render(num) {
    if (this.rendered.has(num)) return;
    this.rendered.add(num);
    const wrap = this.wraps.get(num);
    if (!wrap) return;
    const page = await this.pdf.getPage(num);
    const ratio = window.devicePixelRatio || 1;
    const viewport = page.getViewport({ scale: this.scale * ratio, rotation: this.rotation });
    const cssViewport = page.getViewport({ scale: this.scale, rotation: this.rotation });

    const canvas = document.createElement("canvas");
    canvas.width = viewport.width;
    canvas.height = viewport.height;
    canvas.style.width = `${cssViewport.width}px`;
    canvas.style.height = `${cssViewport.height}px`;
    wrap.appendChild(canvas);
    await page.render({ canvasContext: canvas.getContext("2d"), viewport }).promise;

    const textLayer = document.createElement("div");
    textLayer.className = "text-layer";
    textLayer.style.width = `${cssViewport.width}px`;
    textLayer.style.height = `${cssViewport.height}px`;
    await this._buildTextLayer(page, cssViewport, textLayer, num);
    wrap.appendChild(textLayer);

    if (this.searchRe) this.highlightPage(num);
    this.onPageRendered?.(num, wrap);
  }

  /** Выгружает отрисованную страницу (освобождает canvas), оставляя плейсхолдер. */
  _unload(num) {
    if (!this.rendered.has(num)) return;
    const wrap = this.wraps.get(num);
    if (!wrap) return;
    // освобождаем bitmap canvas явно
    wrap.querySelectorAll("canvas").forEach((c) => { c.width = 0; c.height = 0; });
    wrap.innerHTML = "";
    this.rendered.delete(num);
  }

  /** Гарантирует, что страница отрисована (для перехода/поиска). */
  async ensureRendered(num) {
    if (!this.rendered.has(num)) await this._render(num);
  }

  /** Отрисовать все страницы (для печати). */
  async renderAllForPrint() {
    for (let n = 1; n <= this.numPages; n++) await this.ensureRendered(n);
  }

  async _buildTextLayer(page, viewport, container, num) {
    const tc = await page.getTextContent();
    let plain = "";
    for (const item of tc.items) {
      plain += item.str + (item.hasEOL ? "\n" : " ");
      const tx = pdfjsLib.Util.transform(viewport.transform, item.transform);
      const fontSize = Math.hypot(tx[2], tx[3]);
      const span = document.createElement("span");
      span.textContent = item.str;
      span.style.left = `${tx[4]}px`;
      span.style.top = `${tx[5] - fontSize}px`;
      span.style.fontSize = `${fontSize}px`;
      span.style.fontFamily = item.fontName || "sans-serif";
      container.appendChild(span);
    }
    if (!this.pageText[num - 1]) this.pageText[num - 1] = plain;
  }

  /** Текст по всем страницам (без рендера — для AI/поиска/OCR). */
  async extractAllText() {
    const out = [];
    for (let n = 1; n <= this.numPages; n++) {
      const page = await this.pdf.getPage(n);
      const tc = await page.getTextContent();
      out[n - 1] = tc.items.map((i) => i.str + (i.hasEOL ? "\n" : " ")).join("");
    }
    this.pageText = out;
    return out;
  }

  /** Рендер страницы в отдельный canvas (миниатюры / локальный OCR). */
  async pageToCanvas(index, scale = 2) {
    const page = await this.pdf.getPage(index + 1);
    const viewport = page.getViewport({ scale });
    const canvas = document.createElement("canvas");
    canvas.width = viewport.width;
    canvas.height = viewport.height;
    await page.render({ canvasContext: canvas.getContext("2d"), viewport }).promise;
    return canvas;
  }

  async setScale(scale) {
    this.scale = Math.max(0.25, Math.min(5, scale));
    await this._relayout();
  }

  /** Поворот всех страниц на delta градусов (кратно 90). */
  async setRotation(delta) {
    this.rotation = (((this.rotation + delta) % 360) + 360) % 360;
    await this._relayout();
  }

  /** Пересчёт размеров плейсхолдеров + перерисовка видимых (зум/поворот). */
  async _relayout() {
    for (const [num, wrap] of this.wraps) {
      const { w, h } = this._dims(num);
      wrap.innerHTML = "";
      wrap.style.width = `${w}px`;
      wrap.style.height = `${h}px`;
    }
    this.rendered.clear();
    const top = this.containerEl.scrollTop;
    const bottom = top + this.containerEl.clientHeight;
    for (const [num, wrap] of this.wraps) {
      if (wrap.offsetTop < bottom + 800 && wrap.offsetTop + wrap.offsetHeight > top - 800) {
        await this._render(num);
      }
    }
  }

  async getOutline() {
    if (!this.pdf) return [];
    const outline = await this.pdf.getOutline();
    if (!outline) return [];
    const resolve = async (items) => {
      const res = [];
      for (const it of items) {
        let pageNum = null;
        try {
          if (it.dest) {
            const dest = typeof it.dest === "string" ? await this.pdf.getDestination(it.dest) : it.dest;
            if (dest) pageNum = (await this.pdf.getPageIndex(dest[0])) + 1;
          }
        } catch {
          /* ignore */
        }
        res.push({ title: it.title, page: pageNum, children: it.items?.length ? await resolve(it.items) : [] });
      }
      return res;
    };
    return resolve(outline);
  }

  async scrollToPage(num) {
    const wrap = this.wraps.get(num);
    if (!wrap) return;
    await this.ensureRendered(num);
    wrap.scrollIntoView({ behavior: "smooth", block: "start" });
  }

  // --- Поиск (по извлечённому тексту, работает с ленивым рендером) ---------

  /** Строит RegExp из запроса с опциями {regex, wholeWord, caseSensitive}. null — невалидный. */
  _buildMatcher(query, { regex = false, wholeWord = false, caseSensitive = false } = {}) {
    try {
      let pat = regex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
      if (!regex && wholeWord) pat = `(?:^|\\W)(${pat})(?=\\W|$)`;
      return new RegExp(pat, caseSensitive ? "g" : "gi");
    } catch {
      return null; // невалидное regex-выражение
    }
  }

  /** Возвращает { pages:[стр. с совпадениями], total, error }. */
  search(query, opts = {}) {
    this.clearSearch();
    if (!query || query.length < 2) return { pages: [], total: 0, error: false };
    const re = this._buildMatcher(query, opts);
    if (!re) return { pages: [], total: 0, error: true };
    this.searchRe = re;
    const pages = [];
    let total = 0;
    for (let i = 0; i < this.pageText.length; i++) {
      const m = (this.pageText[i] || "").match(new RegExp(re.source, re.flags));
      if (m && m.length) { pages.push(i + 1); total += m.length; }
    }
    for (const num of pages) if (this.rendered.has(num)) this.highlightPage(num);
    return { pages, total, error: false };
  }

  highlightPage(num) {
    if (!this.searchRe) return;
    const wrap = this.wraps.get(num);
    wrap?.querySelectorAll(".text-layer span").forEach((span) => {
      this.searchRe.lastIndex = 0;
      if (this.searchRe.test(span.textContent)) span.classList.add("search-hit");
    });
  }

  /** Перейти к странице с совпадением и подсветить «текущую». */
  async focusHit(num) {
    await this.scrollToPage(num);
    this.highlightPage(num);
    const wrap = this.wraps.get(num);
    wrap?.querySelectorAll(".search-hit.current").forEach((s) => s.classList.remove("current"));
    const first = wrap?.querySelector(".search-hit");
    if (first) {
      first.classList.add("current");
      first.scrollIntoView({ behavior: "smooth", block: "center" });
    }
  }

  clearSearch() {
    this.searchRe = null;
    this.pagesEl.querySelectorAll(".search-hit").forEach((s) => s.classList.remove("search-hit", "current"));
  }

  _disconnectObservers() {
    this._renderObserver?.disconnect();
    this._pageObserver?.disconnect();
  }
}
