(function () {
  "use strict";

  const FONT_MIN = 80;
  const FONT_MAX = 150;
  const FONT_STEP = 10;
  const PROGRESS_DEBOUNCE_MS = 650;
  const SEARCH_HIGHLIGHT_TYPE = "highlight";
  const SEARCH_HIGHLIGHT_CLASS = "senliz-search-highlight";

  let activeRecord = null;
  let activeBook = null;
  let rendition = null;
  let navigation = null;
  let flattenedToc = [];
  let lastLocation = null;
  let progressSaveTimer = 0;
  let progressSavePromise = Promise.resolve();
  let locationsPromise = null;
  let searchIndexPromise = null;
  let searchIndexController = null;
  let searchResults = [];
  let selectedSearchResult = -1;
  let highlightedCfi = "";
  let noteHistory = [];
  let fontSize = 100;
  let theme = "light";
  let pageTurnInProgress = false;
  let wheelDelta = 0;
  let wheelResetTimer = 0;
  let touchStart = null;

  const viewer = () => document.getElementById("viewer");

  function textValue(value, fallback = "") {
    if (Array.isArray(value)) value = value.filter(Boolean).join(", ");
    const text = String(value || "").replace(/\s+/g, " ").trim();
    return text || fallback;
  }

  function metadataLayout(metadata, book) {
    const candidates = [
      metadata?.layout,
      metadata?.rendition?.layout,
      metadata?.["rendition:layout"],
      book?.packaging?.metadata?.layout,
      book?.packaging?.metadata?.["rendition:layout"]
    ];
    return candidates.some((value) => String(value || "").toLowerCase().includes("pre-paginated"));
  }

  function metadataDirection(metadata, book) {
    const value = [
      metadata?.direction,
      metadata?.pageProgressionDirection,
      metadata?.["page-progression-direction"],
      book?.packaging?.direction,
      book?.packaging?.metadata?.direction
    ].find(Boolean);
    const normalized = String(value || "").toLowerCase();
    if (normalized === "rtl") return "rtl";
    if (normalized === "ltr") return "ltr";
    return "";
  }

  function languageSuggestsRtl(language) {
    return /^(?:fa|ar|he|ur|ps|sd)(?:-|$)/i.test(String(language || ""));
  }

  async function extractCover(book) {
    let coverUrl = "";
    try {
      coverUrl = await book.coverUrl();
      if (!coverUrl) return null;
      const response = await fetch(coverUrl);
      if (!response.ok) return null;
      const blob = await response.blob();
      if (!blob.type.startsWith("image/")) return null;
      return blob;
    } catch (error) {
      console.warn("The EPUB cover could not be extracted.", error);
      return null;
    } finally {
      if (coverUrl?.startsWith("blob:")) URL.revokeObjectURL(coverUrl);
    }
  }

  async function inspect(blob, fileName) {
    const buffer = await blob.arrayBuffer();
    const book = globalThis.ePub(buffer);
    try {
      await book.ready;
      if (book.archived && typeof book.replacements === "function") {
        await book.replacements();
      }
      const metadata = await book.loaded.metadata;
      const title = textValue(metadata?.title, globalThis.SenlizDB.fallbackTitle(fileName));
      const author = textValue(metadata?.creator || metadata?.author, "Unknown Author");
      const language = textValue(metadata?.language);
      const direction = metadataDirection(metadata, book) || (languageSuggestsRtl(language) ? "rtl" : "");
      const coverBlob = await extractCover(book);
      return {
        title,
        author,
        language,
        direction,
        coverBlob,
        identifier: textValue(metadata?.identifier),
        fixedLayout: metadataLayout(metadata, book)
      };
    } finally {
      try {
        book.destroy();
      } catch {
        // A partially opened book may not have resources to release.
      }
    }
  }

  function dispatch(name, detail) {
    document.dispatchEvent(new CustomEvent(name, { detail }));
  }

  function currentCfi() {
    try {
      const location = rendition?.currentLocation?.();
      return location?.start?.cfi || lastLocation?.start?.cfi || "";
    } catch {
      return lastLocation?.start?.cfi || "";
    }
  }

  function canonicalHref(href) {
    return String(href || "").split("#")[0].replace(/^\.?\//, "");
  }

  function normalizeBookPath(value) {
    const source = String(value || "")
      .replace(/\\/g, "/")
      .split("?")[0]
      .replace(/^\/+/, "");
    const output = [];
    for (const part of source.split("/")) {
      if (!part || part === ".") continue;
      if (part === "..") output.pop();
      else output.push(part);
    }
    return output.join("/");
  }

  function decodedBookPath(value) {
    try {
      return decodeURIComponent(normalizeBookPath(value));
    } catch {
      return normalizeBookPath(value);
    }
  }

  function resolveBookTarget(target, baseHref = "") {
    const value = String(target || "").trim();
    if (!value || /^epubcfi\(/i.test(value)) return value;
    if (/^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("//")) return "";

    const hashIndex = value.indexOf("#");
    const rawPath = hashIndex >= 0 ? value.slice(0, hashIndex) : value;
    const fragment = hashIndex >= 0 ? value.slice(hashIndex) : "";
    const basePath = normalizeBookPath(String(baseHref || "").split("#")[0]);
    const baseDirectory = basePath.includes("/") ? basePath.slice(0, basePath.lastIndexOf("/") + 1) : "";
    const candidatePath = normalizeBookPath(rawPath ? `${baseDirectory}${rawPath}` : basePath);
    const decodedCandidate = decodedBookPath(candidatePath);
    const spine = activeBook?.spine?.spineItems || [];

    const exact = spine.find((section) => decodedBookPath(section.href || section.url) === decodedCandidate);
    if (exact) return `${exact.href}${fragment}`;

    const suffixMatches = spine.filter((section) => {
      const sectionPath = decodedBookPath(section.href || section.url);
      return sectionPath.endsWith(`/${decodedCandidate}`) || decodedCandidate.endsWith(`/${sectionPath}`);
    });
    if (suffixMatches.length === 1) return `${suffixMatches[0].href}${fragment}`;

    const candidateName = decodedCandidate.split("/").pop();
    const basenameMatches = spine.filter((section) => {
      const sectionName = decodedBookPath(section.href || section.url).split("/").pop();
      return candidateName && sectionName === candidateName;
    });
    if (basenameMatches.length === 1) return `${basenameMatches[0].href}${fragment}`;

    return `${candidatePath}${fragment}`;
  }

  function tocLabelForHref(href) {
    const canonical = canonicalHref(href);
    const exact = flattenedToc.find((item) => canonicalHref(item.href) === canonical);
    if (exact) return String(exact.label || "").trim();
    const near = flattenedToc.find((item) => {
      const candidate = canonicalHref(item.href);
      return canonical.endsWith(candidate) || candidate.endsWith(canonical);
    });
    return String(near?.label || "").trim();
  }

  function officialPercentage(cfi, location) {
    try {
      if (cfi && activeBook?.locations?.length?.()) {
        const percentage = activeBook.locations.percentageFromCfi(cfi);
        if (Number.isFinite(percentage)) return Math.max(0, Math.min(1, percentage));
      }
    } catch {
      // Fall through to the renderer-provided percentage.
    }

    const renderedPercentage = Number(location?.start?.percentage);
    if (Number.isFinite(renderedPercentage)) return Math.max(0, Math.min(1, renderedPercentage));

    const spineIndex = Number(location?.start?.index);
    const spineLength = activeBook?.spine?.spineItems?.length || 0;
    if (Number.isFinite(spineIndex) && spineLength > 1) {
      return Math.max(0, Math.min(1, spineIndex / (spineLength - 1)));
    }
    return 0;
  }

  function queueProgressSave(location) {
    clearTimeout(progressSaveTimer);
    const cfi = location?.start?.cfi;
    if (!activeRecord?.id || !cfi) return;

    const percentage = officialPercentage(cfi, location);
    const chapter = tocLabelForHref(location.start.href) || `Section ${(location.start.index || 0) + 1}`;
    dispatch("senliz-save-status", { state: "saving", message: "Saving…" });

    progressSaveTimer = setTimeout(() => {
      progressSavePromise = globalThis.SenlizDB.putProgress({
        bookId: activeRecord.id,
        cfi,
        href: location.start.href || "",
        chapter,
        percentage,
        lastReadTimestamp: new Date().toISOString()
      })
        .then(() => {
          dispatch("senliz-save-status", { state: "saved", message: "Saved on this device" });
        })
        .catch((error) => {
          console.error("Reading progress could not be saved.", error);
          dispatch("senliz-save-status", {
            state: "error",
            message: "Progress was not saved.",
            error
          });
        });
    }, PROGRESS_DEBOUNCE_MS);
  }

  async function flushProgress() {
    if (progressSaveTimer) {
      clearTimeout(progressSaveTimer);
      progressSaveTimer = 0;
      const location = lastLocation;
      const cfi = location?.start?.cfi;
      if (activeRecord?.id && cfi) {
        const percentage = officialPercentage(cfi, location);
        const chapter = tocLabelForHref(location.start.href) || `Section ${(location.start.index || 0) + 1}`;
        progressSavePromise = globalThis.SenlizDB.putProgress({
          bookId: activeRecord.id,
          cfi,
          href: location.start.href || "",
          chapter,
          percentage,
          lastReadTimestamp: new Date().toISOString()
        });
      }
    }
    await progressSavePromise;
  }

  function announceLocation(location) {
    const cfi = location?.start?.cfi || "";
    const percentage = officialPercentage(cfi, location);
    const chapter = tocLabelForHref(location?.start?.href) || `Section ${(location?.start?.index || 0) + 1}`;
    const page = Number(location?.start?.displayed?.page || 0);
    const pages = Number(location?.start?.displayed?.total || 0);
    dispatch("senliz-location", {
      cfi,
      href: location?.start?.href || "",
      chapter,
      percentage,
      page: Number.isFinite(page) && page > 0 ? page : 0,
      pages: Number.isFinite(pages) && pages > 0 ? pages : 0,
      atStart: Boolean(location?.atStart),
      atEnd: Boolean(location?.atEnd)
    });
  }

  function handleRelocated(location) {
    lastLocation = location;
    announceLocation(location);
    queueProgressSave(location);
  }

  function themeRules(themeName) {
    const dark = themeName === "dark";
    const background = dark ? "#0d0e0e" : "#f3f0e8";
    const foreground = dark ? "#eceae4" : "#191814";
    const muted = dark ? "#aaa8a1" : "#66645c";
    const line = dark ? "#3b3d3d" : "#d7d2c6";
    const accent = dark ? "#d67468" : "#a5382f";

    return {
      "html": {
        "background": `${background} !important`,
        "color-scheme": dark ? "dark" : "light"
      },
      "body": {
        "box-sizing": "border-box",
        "max-width": "none !important",
        "margin": "0 !important",
        "padding": "clamp(46px, 7vh, 78px) clamp(28px, 5vw, 48px) 64px !important",
        "background": `${background} !important`,
        "color": `${foreground} !important`,
        "font-family": "Georgia, 'Times New Roman', serif !important",
        "line-height": "1.86 !important",
        "overflow-wrap": "break-word",
        "text-rendering": "optimizeLegibility"
      },
      "body, p, li, dd, dt, blockquote": {
        "text-align": "start !important"
      },
      "p": {
        "margin-top": "0",
        "margin-bottom": "1.4em"
      },
      "h1, h2, h3, h4, h5, h6": {
        "line-height": "1.25 !important",
        "break-after": "avoid",
        "page-break-after": "avoid"
      },
      "img, svg, video": {
        "max-width": "100% !important",
        "height": "auto !important"
      },
      "table": {
        "max-width": "100%",
        "border-collapse": "collapse"
      },
      "pre, code": {
        "white-space": "pre-wrap",
        "overflow-wrap": "anywhere"
      },
      "blockquote": {
        "margin-inline": "0",
        "padding-inline-start": "1.2em",
        "border-inline-start": `2px solid ${line}`,
        "color": muted
      },
      "a": {
        "color": `${accent} !important`,
        "text-decoration-thickness": "1px",
        "text-underline-offset": "3px"
      },
      ":lang(fa), :lang(ar), [lang^='fa'], [lang^='ar'], [dir='rtl']": {
        "font-family": "Tahoma, 'Geeza Pro', 'Noto Naskh Arabic', Arial, sans-serif !important",
        "direction": "rtl",
        "text-align": "start !important",
        "line-height": "2 !important"
      },
      "[dir='ltr']": {
        "direction": "ltr"
      },
      "::selection": {
        "background": dark ? "#70463f" : "#e7bbb4"
      },
      ".senliz-search-highlight": {
        "background": dark ? "#835f20 !important" : "#ffe28a !important",
        "color": `${foreground} !important`
      }
    };
  }

  function installThemeRules() {
    if (!rendition) return;
    rendition.themes.register("senliz-light", themeRules("light"));
    rendition.themes.register("senliz-dark", themeRules("dark"));
    rendition.themes.select(theme === "dark" ? "senliz-dark" : "senliz-light");
    rendition.themes.fontSize(`${fontSize}%`);
  }

  function configureContent(contents) {
    globalThis.SenlizSecurity.sanitizeDocument(contents.document);
    globalThis.SenlizSecurity.attachLinkGuard(contents, {
      onExternal(url) {
        dispatch("senliz-external-request", { url });
      },
      onInternal(rawHref) {
        const currentHref =
          contents?.section?.href ||
          activeBook?.spine?.spineItems?.[currentSpineIndex()]?.href ||
          lastLocation?.start?.href ||
          "";
        const target = resolveBookTarget(rawHref, currentHref);
        if (target) displayTarget(target);
      }
    });
    installPageTurnEvents(contents.window);
    const language = contents.document.documentElement.getAttribute("lang") || activeRecord?.language || "";
    if (language) contents.document.documentElement.setAttribute("lang", language);
    if (!contents.document.documentElement.getAttribute("dir") && activeRecord?.direction) {
      contents.document.documentElement.setAttribute("dir", activeRecord.direction);
    }
  }

  async function open(record) {
    await close({ keepView: true });
    activeRecord = record;
    noteHistory = [];
    searchResults = [];
    selectedSearchResult = -1;
    highlightedCfi = "";
    lastLocation = null;
    searchIndexPromise = null;
    searchIndexController = null;
    dispatch("senliz-note-history", { available: false });

    if (!(record?.epubBlob instanceof Blob)) {
      throw new Error("This book needs to be imported again before it can be opened.");
    }

    if (record.fixedLayout) {
      throw new Error("This fixed-layout EPUB is not currently supported by Senliz Library.");
    }

    const progress = await globalThis.SenlizDB.getProgress(record.id);
    fontSize = Number(await globalThis.SenlizDB.getPreference("fontSize", 100));
    theme = (await globalThis.SenlizDB.getPreference("theme", "light")) === "dark" ? "dark" : "light";
    fontSize = Math.max(FONT_MIN, Math.min(FONT_MAX, fontSize));

    const arrayBuffer = await record.epubBlob.arrayBuffer();
    activeBook = globalThis.ePub(arrayBuffer);
    await activeBook.ready;
    const metadata = await activeBook.loaded.metadata;
    if (metadataLayout(metadata, activeBook)) {
      throw new Error("This fixed-layout EPUB is not currently supported by Senliz Library.");
    }

    navigation = await activeBook.loaded.navigation;
    flattenedToc = globalThis.SenlizSearch.flattenNavigation(navigation?.toc || []);
    dispatch("senliz-reader-metadata", {
      title: textValue(metadata?.title, record.title),
      author: textValue(metadata?.creator || metadata?.author, record.author),
      toc: flattenedToc
    });

    rendition = activeBook.renderTo(viewer(), {
      width: "100%",
      height: "100%",
      manager: "default",
      flow: "paginated",
      spread: "none",
      minSpreadWidth: 99999,
      allowScriptedContent: false
    });

    rendition.hooks.content.register(configureContent);
    installThemeRules();
    rendition.on("relocated", handleRelocated);
    rendition.on("click", (event, contents) => {
      const anchor = event?.target?.closest?.("a[href],area[href]");
      if (!anchor) return;
      const rawHref = anchor.getAttribute("href") || "";
      if (!rawHref || /^[a-z][a-z0-9+.-]*:/i.test(rawHref)) return;
      const origin =
        currentCfi() ||
        lastLocation?.start?.href ||
        contents?.section?.href ||
        activeBook?.spine?.spineItems?.[currentSpineIndex()]?.href ||
        "";
      if (origin) {
        noteHistory.push(origin);
        dispatch("senliz-note-history", { available: true });
      }
    });
    rendition.on("rendered", (_section, view) => {
      if (view?.contents) configureContent(view.contents);
    });
    rendition.on("displayError", (error) => {
      console.error("EPUB display error.", error);
      dispatch("senliz-reader-error", {
        message: "This chapter could not be displayed.",
        error
      });
    });
    rendition.on("resized", () => {
      try {
        rendition.reportLocation();
      } catch {
        // epub.js will emit the next location during normal scrolling.
      }
    });

    installPageTurnEvents(viewer());
    await rendition.display(progress?.cfi || undefined);
    try {
      await globalThis.SenlizDB.updateBook(record.id, { lastOpenedDate: new Date().toISOString() });
    } catch (error) {
      console.warn("The last-opened date could not be saved.", error);
    }

    locationsPromise = activeBook.locations
      .generate(1600)
      .then(() => {
        const location = rendition?.currentLocation?.();
        if (location?.start?.cfi) announceLocation(location);
      })
      .catch((error) => {
        console.warn("EPUB locations could not be generated.", error);
      });

    dispatch("senliz-reader-ready", { id: record.id });
  }

  async function close(options = {}) {
    clearTimeout(progressSaveTimer);
    if (activeRecord) {
      try {
        await flushProgress();
      } catch (error) {
        console.error("The final reading location could not be saved.", error);
      }
    }

    searchIndexController?.abort();
    searchIndexController = null;
    searchIndexPromise = null;
    searchResults = [];
    selectedSearchResult = -1;
    noteHistory = [];

    if (rendition) {
      try {
        rendition.destroy();
      } catch {
        // Continue releasing the book even if the rendition is already gone.
      }
    }
    if (activeBook) {
      try {
        activeBook.destroy();
      } catch {
        // A failed book may only be partially initialised.
      }
    }

    rendition = null;
    activeBook = null;
    activeRecord = null;
    navigation = null;
    flattenedToc = [];
    lastLocation = null;
    locationsPromise = null;
    highlightedCfi = "";
    pageTurnInProgress = false;
    wheelDelta = 0;
    touchStart = null;
    clearTimeout(wheelResetTimer);
    wheelResetTimer = 0;
    viewer().replaceChildren();
    if (!options.keepView) dispatch("senliz-reader-closed", {});
  }

  async function setTheme(nextTheme) {
    theme = nextTheme === "dark" ? "dark" : "light";
    await globalThis.SenlizDB.setPreference("theme", theme);
    if (!rendition) return;
    const cfi = currentCfi();
    rendition.themes.select(theme === "dark" ? "senliz-dark" : "senliz-light");
    if (cfi) await rendition.display(cfi);
    rendition.reportLocation();
  }

  async function changeFont(delta) {
    const next = Math.max(FONT_MIN, Math.min(FONT_MAX, fontSize + delta));
    if (next === fontSize) return fontSize;
    const cfi = currentCfi();
    fontSize = next;
    await globalThis.SenlizDB.setPreference("fontSize", fontSize);
    if (rendition) {
      rendition.themes.fontSize(`${fontSize}%`);
      if (cfi) await rendition.display(cfi);
      rendition.reportLocation();
    }
    return fontSize;
  }

  function currentSpineIndex() {
    const cfi = currentCfi();
    if (cfi) {
      try {
        const section = activeBook?.spine?.get(cfi);
        if (Number.isInteger(section?.index)) return section.index;
      } catch {
        // Fall back to the current rendition location.
      }
    }
    return Number(lastLocation?.start?.index || 0);
  }

  async function turnPage(direction) {
    if (!rendition || pageTurnInProgress) return false;
    pageTurnInProgress = true;
    try {
      if (direction < 0) await rendition.prev();
      else await rendition.next();
      rendition.reportLocation();
      return true;
    } catch (error) {
      console.error("EPUB page could not be turned.", error);
      dispatch("senliz-reader-error", { message: "This page could not be displayed.", error });
      return false;
    } finally {
      pageTurnInProgress = false;
    }
  }

  function previousPage() {
    return turnPage(-1);
  }

  function nextPage() {
    return turnPage(1);
  }

  function onPageWheel(event) {
    const delta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY;
    if (!Number.isFinite(delta) || delta === 0) return;
    event.preventDefault();
    clearTimeout(wheelResetTimer);
    wheelDelta += delta;
    wheelResetTimer = setTimeout(() => {
      wheelDelta = 0;
    }, 180);
    if (Math.abs(wheelDelta) < 44) return;
    const direction = wheelDelta > 0 ? 1 : -1;
    wheelDelta = 0;
    turnPage(direction);
  }

  function onTouchStart(event) {
    const touch = event.touches?.[0];
    if (!touch) return;
    touchStart = { x: touch.clientX, y: touch.clientY };
  }

  function onTouchEnd(event) {
    if (!touchStart) return;
    const touch = event.changedTouches?.[0];
    const start = touchStart;
    touchStart = null;
    if (!touch) return;
    const deltaX = touch.clientX - start.x;
    const deltaY = touch.clientY - start.y;
    if (Math.abs(deltaX) < 46 || Math.abs(deltaX) < Math.abs(deltaY) * 1.2) return;
    turnPage(deltaX < 0 ? 1 : -1);
  }

  function onReaderKey(event) {
    if (event.target?.matches?.("input, textarea, [contenteditable='true']")) return;
    if (event.key === "ArrowLeft" || event.key === "PageUp") {
      event.preventDefault();
      previousPage();
    } else if (event.key === "ArrowRight" || event.key === "PageDown" || event.key === " ") {
      event.preventDefault();
      nextPage();
    }
  }

  function installPageTurnEvents(target) {
    if (!target || target.__senlizPageTurnEvents) return;
    Object.defineProperty(target, "__senlizPageTurnEvents", { value: true });
    target.addEventListener("wheel", onPageWheel, { passive: false });
    target.addEventListener("touchstart", onTouchStart, { passive: true });
    target.addEventListener("touchend", onTouchEnd, { passive: true });
    target.addEventListener("keydown", onReaderKey);
  }

  async function displayTarget(target, options = {}) {
    if (!rendition || !target) return false;
    if (options.remember !== false) {
      const origin =
        currentCfi() ||
        lastLocation?.start?.href ||
        activeBook?.spine?.spineItems?.[currentSpineIndex()]?.href ||
        "";
      if (origin) {
        noteHistory.push(origin);
        dispatch("senliz-note-history", { available: true });
      }
    }
    try {
      const resolved = resolveBookTarget(target);
      if (!resolved) return false;
      await rendition.display(resolved);
      dispatch("senliz-note-history", { available: noteHistory.length > 0 });
      return true;
    } catch (error) {
      console.error("EPUB target could not be opened.", { target, error });
      dispatch("senliz-reader-error", { message: "This link could not be opened.", error });
      return false;
    }
  }

  async function backFromNote() {
    const target = noteHistory.pop();
    if (!target) return false;
    await rendition.display(target);
    dispatch("senliz-note-history", { available: noteHistory.length > 0 });
    return true;
  }

  function ensureSearchIndex() {
    if (searchIndexPromise) return searchIndexPromise;
    if (!activeBook) return Promise.resolve([]);
    searchIndexController = new AbortController();
    dispatch("senliz-search-index", { state: "working", completed: 0, total: activeBook.spine.spineItems.length });
    searchIndexPromise = globalThis.SenlizSearch.buildIndex(activeBook, navigation, {
      signal: searchIndexController.signal,
      onProgress(progress) {
        dispatch("senliz-search-index", { state: "working", ...progress });
      }
    })
      .then((index) => {
        dispatch("senliz-search-index", { state: "ready", completed: index.length, total: index.length });
        return index;
      })
      .catch((error) => {
        if (error?.name !== "AbortError") {
          console.error("Book search index failed.", error);
          dispatch("senliz-search-index", { state: "error", error });
        }
        throw error;
      });
    return searchIndexPromise;
  }

  async function search(query, signal) {
    const normalized = globalThis.SenlizSearch.normalizeForSearch(query);
    if (normalized.length < 2) {
      searchResults = [];
      selectedSearchResult = -1;
      return [];
    }
    const index = await ensureSearchIndex();
    searchResults = await globalThis.SenlizSearch.searchBook(activeBook, index, query, {
      signal,
      limit: 200
    });
    selectedSearchResult = searchResults.length ? 0 : -1;
    return searchResults;
  }

  function clearHighlight() {
    if (!highlightedCfi || !rendition) return;
    try {
      rendition.annotations.remove(highlightedCfi, SEARCH_HIGHLIGHT_TYPE);
    } catch {
      // The highlight may have already been discarded with an unloaded section.
    }
    highlightedCfi = "";
  }

  async function showSearchResult(index) {
    const result = searchResults[index];
    if (!result || !rendition) return null;
    clearHighlight();
    selectedSearchResult = index;
    await rendition.display(result.cfi);
    highlightedCfi = result.cfi;
    try {
      rendition.annotations.highlight(
        result.cfi,
        {},
        undefined,
        SEARCH_HIGHLIGHT_CLASS,
        {
          fill: theme === "dark" ? "#b38724" : "#f3cb4c",
          "fill-opacity": "0.45",
          "mix-blend-mode": "multiply"
        }
      );
    } catch (error) {
      console.warn("The search result could not be highlighted.", error);
    }
    return { ...result, index };
  }

  async function stepSearchResult(delta) {
    if (!searchResults.length) return null;
    const next = (selectedSearchResult + delta + searchResults.length) % searchResults.length;
    return showSearchResult(next);
  }

  function toc() {
    return flattenedToc.slice();
  }

  function state() {
    return {
      isOpen: Boolean(activeRecord && rendition),
      bookId: activeRecord?.id || "",
      fontSize,
      theme,
      searchResultCount: searchResults.length,
      selectedSearchResult
    };
  }

  globalThis.SenlizReader = Object.freeze({
    inspect,
    open,
    close,
    setTheme,
    changeFont,
    previousPage,
    nextPage,
    displayTarget,
    backFromNote,
    ensureSearchIndex,
    search,
    showSearchResult,
    stepSearchResult,
    toc,
    state,
    constants: Object.freeze({ FONT_MIN, FONT_MAX, FONT_STEP })
  });
})();
