<script th:fragment="toc-scripts" th:inline="javascript">
  (function () {
    function generateCatalog() {
      const article = document.querySelector(".article") || document.getElementById("article-content");
      const catalogWidget = document.getElementById("catalog-widget");
      const catalogContent = document.getElementById("catalog-content");
      const noTocTip = document.getElementById("no-toc-tip");

      if (!article || !catalogWidget || !catalogContent) return;

      const headers = Array.from(article.querySelectorAll("h1, h2, h3, h4, h5, h6"));
      if (headers.length === 0) {
        catalogWidget.style.display = "block";
        catalogContent.style.display = "none";
        noTocTip.style.display = "block";
        return;
      }

      catalogWidget.style.display = "block";
      catalogContent.style.display = "block";
      noTocTip.style.display = "none";
      catalogContent.innerHTML = "";

      const root = { children: [] };
      const stack = [root];

      headers.forEach((header, index) => {
        if (!header.id) header.id = "heading-" + index;

        const level = parseInt(header.tagName.substring(1));
        const item = {
          id: header.id,
          text: header.textContent,
          level: level,
          children: [],
        };

        while (stack.length > 1 && stack[stack.length - 1].level >= level) {
          stack.pop();
        }

        stack[stack.length - 1].children.push(item);
        stack.push(item);
      });

      function renderTree(items) {
        if (!items.length) return null;
        const ol = document.createElement("ol");
        items.forEach((item) => {
          const li = document.createElement("li");
          li.dataset.id = item.id;
          li.dataset.level = String(item.level || 1);

          const hasChildren = item.children && item.children.length > 0;
          if (hasChildren) {
            li.classList.add("toc-collapsed");
          }

          const wrapper = document.createElement("div");
          wrapper.className = "toc-item-wrapper";

          const a = document.createElement("a");
          a.href = "#" + item.id;
          a.textContent = item.text;
          a.title = item.text;
          a.onclick = (e) => {
            e.preventDefault();
            const target = document.getElementById(item.id);
            if (target) {
              target.scrollIntoView({ behavior: "smooth" });
              history.pushState(null, null, "#" + item.id);
              target.classList.remove("toc-highlight");
              void target.offsetWidth;
              target.classList.add("toc-highlight");
              setTimeout(() => target.classList.remove("toc-highlight"), 2000);
              setActiveById(item.id);
            }
          };

          wrapper.appendChild(a);

          if (hasChildren) {
            const toggle = document.createElement("span");
            toggle.className = "toc-toggle";
            toggle.innerHTML = '<span class="icon-[ph--caret-right-bold]"></span>';
            toggle.onclick = (e) => {
              e.preventDefault();
              e.stopPropagation();
              toggleCollapse(li);
            };
            wrapper.appendChild(toggle);
          }
          li.appendChild(wrapper);

          const childrenOl = renderTree(item.children);
          if (childrenOl) {
            childrenOl.className = "toc-children";
            li.appendChild(childrenOl);
          }
          ol.appendChild(li);
        });
        return ol;
      }

      function toggleCollapse(li) {
        if (li.classList.contains("toc-collapsed")) {
          li.classList.remove("toc-collapsed");
          li.classList.add("toc-expanded");
        } else {
          li.classList.remove("toc-expanded");
          li.classList.add("toc-collapsed");
        }
      }

      function expandLi(li) {
        li.classList.remove("toc-collapsed");
        li.classList.add("toc-expanded");
      }

      function collapseLi(li) {
        li.classList.remove("toc-expanded");
        li.classList.add("toc-collapsed");
      }

      function getAllParentLis(currentLi) {
        const parents = [];
        let parent = currentLi.parentElement?.closest("li");
        while (parent) {
          parents.push(parent);
          parent = parent.parentElement?.closest("li");
        }
        return parents;
      }

      function getVisibleHeaderRange() {
        const viewportTop = window.scrollY + 80;
        const viewportBottom = window.scrollY + window.innerHeight - 80;

        let firstVisibleId = null;
        let lastVisibleId = null;

        for (let i = 0; i < headers.length; i++) {
          const header = headers[i];
          const top = header.offsetTop;
          const bottom = top + header.offsetHeight;

          if (top <= viewportBottom && bottom >= viewportTop) {
            if (firstVisibleId === null) {
              firstVisibleId = header.id;
            }
            lastVisibleId = header.id;
          }
        }

        return { firstVisibleId, lastVisibleId };
      }

      function autoCollapseByViewport() {
        let { firstVisibleId, lastVisibleId } = getVisibleHeaderRange();

        if (firstVisibleId === null || lastVisibleId === null) {
          const currentId = resolveCurrentHeadingId();
          firstVisibleId = currentId;
          lastVisibleId = currentId;
        }

        const firstLi = tocItemMap.get(firstVisibleId);
        const lastLi = tocItemMap.get(lastVisibleId);

        const allParentIds = new Set();

        if (firstLi) {
          allParentIds.add(firstLi.dataset.id);
          getAllParentLis(firstLi).forEach((li) => {
            if (li.dataset.id) allParentIds.add(li.dataset.id);
          });
        }

        if (lastLi && lastLi !== firstLi) {
          allParentIds.add(lastLi.dataset.id);
          getAllParentLis(lastLi).forEach((li) => {
            if (li.dataset.id) allParentIds.add(li.dataset.id);
          });
        }

        catalogContent.querySelectorAll("li.toc-expanded").forEach((li) => {
          const id = li.dataset.id;
          if (!allParentIds.has(id)) {
            const hasVisibleChildren = hasVisibleChildrenInViewport(li);
            if (!hasVisibleChildren) {
              collapseLi(li);
            }
          }
        });

        allParentIds.forEach((id) => {
          const li = tocItemMap.get(id);
          if (li && !li.classList.contains("toc-expanded")) {
            expandLi(li);
          }
        });
      }

      function hasVisibleChildrenInViewport(parentLi) {
        const children = parentLi.querySelectorAll(":scope > .toc-children > ol > li");
        if (children.length === 0) return false;

        const viewportTop = window.scrollY + 80;
        const viewportBottom = window.scrollY + window.innerHeight - 80;

        for (const child of children) {
          const childId = child.dataset.id;
          const header = document.getElementById(childId);
          if (header) {
            const top = header.offsetTop;
            const bottom = top + header.offsetHeight;
            if (top <= viewportBottom && bottom >= viewportTop) {
              return true;
            }
          }
        }

        return false;
      }

      const treeDom = renderTree(root.children);
      if (treeDom) catalogContent.appendChild(treeDom);

      const tocItemMap = new Map();
      catalogContent.querySelectorAll("li[data-id]").forEach((li) => {
        tocItemMap.set(li.dataset.id, li);
      });

      function ensureActiveVisible(currentLi) {
        if (!currentLi) return;
        const viewTop = catalogContent.scrollTop;
        const viewBottom = viewTop + catalogContent.clientHeight;
        const itemTop = currentLi.offsetTop;
        const itemBottom = itemTop + currentLi.offsetHeight;
        const padding = 24;

        if (itemTop < viewTop + padding || itemBottom > viewBottom - padding) {
          const targetTop = Math.max(0, itemTop - catalogContent.clientHeight * 0.35);
          const distance = Math.abs(targetTop - catalogContent.scrollTop);
          catalogContent.scrollTo({
            top: targetTop,
            behavior: distance > 80 ? "smooth" : "auto",
          });
        }
      }

      function getArticleScrollProgress() {
        const maxTocScroll = catalogContent.scrollHeight - catalogContent.clientHeight;
        if (maxTocScroll <= 0) return 0;

        const articleTop = article.offsetTop;
        const articleBottom = articleTop + article.offsetHeight;
        const articleScrollRange = Math.max(1, articleBottom - window.innerHeight - articleTop);
        const progress = (window.scrollY - articleTop) / articleScrollRange;

        return Math.min(1, Math.max(0, progress));
      }

      function syncCatalogScrollWithArticle() {
        const maxTocScroll = catalogContent.scrollHeight - catalogContent.clientHeight;
        if (maxTocScroll <= 0) return;

        catalogContent.scrollTop = maxTocScroll * getArticleScrollProgress();
      }

      function setActiveById(id, options = {}) {
        if (!id) return;
        const keepVisible = options.keepVisible !== false;

        catalogContent.querySelectorAll("li.active, li.has-active").forEach((el) => {
          el.classList.remove("active", "has-active");
        });

        const currentLi = tocItemMap.get(id);
        if (!currentLi) return;

        currentLi.classList.add("active");
        let parent = currentLi.parentElement?.closest("li");
        while (parent) {
          parent.classList.add("has-active");
          expandLi(parent);
          parent = parent.parentElement?.closest("li");
        }

        if (keepVisible) {
          ensureActiveVisible(currentLi);
        }
      }

      function resolveCurrentHeadingId() {
        const marker = window.scrollY + 140;
        let currentId = headers[0].id;

        for (let i = 0; i < headers.length; i++) {
          if (headers[i].offsetTop <= marker) {
            currentId = headers[i].id;
          } else {
            break;
          }
        }
        return currentId;
      }

      function syncActiveByScroll() {
        setActiveById(resolveCurrentHeadingId(), { keepVisible: false });
      }

      let rafId = 0;
      let lastViewportRange = null;

      const onScroll = () => {
        if (rafId) return;
        rafId = window.requestAnimationFrame(() => {
          rafId = 0;
          syncActiveByScroll();
          autoCollapseByViewport();
          syncCatalogScrollWithArticle();
        });
      };

      window.addEventListener("scroll", onScroll, { passive: true });
      window.addEventListener("resize", onScroll);
      syncActiveByScroll();
      syncCatalogScrollWithArticle();
    }

    generateCatalog();
  })();
</script>
