/**
 * Collapsible Table of Contents for mkdocs-material with toc.integrate.
 *
 * Behaviour:
 *  - ALL nested TOC items start collapsed on page load.
 *  - Clicking a parent item toggles it and marks it as "user-pinned" so it
 *    stays in whatever state the user chose.
 *  - As the reader scrolls, the ancestors of the currently-active TOC link
 *    are automatically expanded so the highlighted item is always visible.
 *  - When the reader scrolls past a section, any items that were only
 *    auto-expanded (not user-pinned) collapse back down.
 *  - Works with mkdocs-material instant-loading / SPA navigation.
 *
 * Page navigation is handled by the top navbar tabs + dropdowns.
 * This script only manages the TOC (md-nav--secondary) in the left sidebar.
 */
(function () {
  "use strict";

  /**
   * Set of TOC <li> elements the user has manually expanded.
   * These survive auto-collapse and are only cleared on page navigation.
   */
  var userPinned = new Set();

  /**
   * Set of TOC <li> elements that were auto-expanded by the scroll tracker.
   * Cleared and recalculated each time the active link changes.
   */
  var autoExpanded = new Set();

  /** Reference to the sidebar element (cached after first lookup). */
  var sidebar = null;

  // ── Helpers ──────────────────────────────────────────────────────────

  /**
   * Return all ancestor <li.md-nav__item--nested> elements between `el`
   * and the TOC root (the <ul data-md-component="toc"> list).
   */
  function ancestorItems(el) {
    var ancestors = [];
    var node = el ? el.parentElement : null;
    while (node && node !== sidebar) {
      if (
        node.tagName === "LI" &&
        node.classList.contains("md-nav__item--nested")
      ) {
        ancestors.push(node);
      }
      node = node.parentElement;
    }
    return ancestors;
  }

  /**
   * Collapse a nested item (add the CSS class that hides children).
   */
  function collapse(li) {
    li.classList.add("toc-collapsed");
  }

  /**
   * Expand a nested item (remove the CSS class so children are visible).
   */
  function expand(li) {
    li.classList.remove("toc-collapsed");
  }

  // ── Shared: make nested items collapsible ────────────────────────────
  //
  // Scans `containerEl` for <li> items with nested <nav> children and
  // turns them into collapsible items: adds the --nested class, collapses
  // them, and injects a toggle chevron.

  function makeItemsCollapsible(containerEl) {
    containerEl.querySelectorAll("li.md-nav__item").forEach(function (li) {
      var nestedNav = li.querySelector(":scope > nav.md-nav, :scope > .md-nav");
      if (!nestedNav) return;

      li.classList.add("md-nav__item--nested");

      // Collapse everything on load
      collapse(li);

      // Inject a dedicated toggle button (chevron) into the link row.
      // Clicking the chevron only toggles expand/collapse.
      // Clicking the link text still navigates to the heading.
      var link = li.querySelector(":scope > a.md-nav__link");
      if (link && !link.querySelector(".toc-toggle")) {
        var toggle = document.createElement("span");
        toggle.className = "toc-toggle";
        toggle.setAttribute("role", "button");
        toggle.setAttribute("aria-label", "Toggle section");
        link.appendChild(toggle);

        toggle.addEventListener("click", function (e) {
          // Stop the click from propagating to the <a> and navigating
          e.preventDefault();
          e.stopPropagation();

          var isCollapsed = li.classList.contains("toc-collapsed");
          if (isCollapsed) {
            expand(li);
            userPinned.add(li);
          } else {
            collapse(li);
            userPinned.delete(li);
          }
        });
      }
    });
  }

  // ── Initialisation ──────────────────────────────────────────────────

  function initCollapsibleToc() {
    sidebar = document.querySelector(".md-sidebar--primary");
    if (!sidebar) return;

    // Reset pinned / auto-expanded tracking on (re-)init
    userPinned.clear();
    autoExpanded.clear();

    // Force the TOC visible.  Material's base CSS hides the secondary
    // nav via a sibling-combinator rule keyed off the #__toc checkbox.
    // We force-check it and set inline display so the TOC is guaranteed
    // visible regardless of the CSS cascade.
    var tocCheckbox = sidebar.querySelector("#__toc");
    if (tocCheckbox) {
      tocCheckbox.checked = true;
    }

    // Find the actual TOC (md-nav--secondary).  We scope to the secondary
    // nav to avoid touching the page-level nav items that wrap sections.
    var tocNav = sidebar.querySelector("nav.md-nav--secondary");
    if (!tocNav) return;

    tocNav.style.display = "block";
    tocNav.style.opacity = "1";
    tocNav.style.visibility = "visible";

    // Make all nested TOC items collapsible, with pinning support for
    // the active-link watcher.
    makeItemsCollapsible(tocNav);

    // Kick off the active-link watcher
    watchActiveLink();
  }

  // ── Active-link watcher ─────────────────────────────────────────────
  //
  // mkdocs-material dynamically toggles `.md-nav__link--active` on the
  // TOC link that corresponds to the heading currently in view.  We use
  // a MutationObserver on the TOC container to detect when that class
  // changes and react by expanding / collapsing as needed.

  var observer = null;

  function watchActiveLink() {
    // Disconnect any previous observer (e.g. after SPA navigation)
    if (observer) {
      observer.disconnect();
      observer = null;
    }

    var tocRoot = sidebar
      ? sidebar.querySelector("[data-md-component='toc']")
      : null;
    if (!tocRoot) return;

    // Respond to the current active link immediately
    handleActiveChange(tocRoot);

    // Observe subtree attribute changes — the theme adds / removes
    // the --active class on <a> elements as the user scrolls.
    observer = new MutationObserver(function () {
      handleActiveChange(tocRoot);
    });

    observer.observe(tocRoot, {
      subtree: true,
      attributes: true,
      attributeFilter: ["class"],
    });
  }

  /**
   * Called whenever the active TOC link may have changed.
   *
   * 1. Find the currently-active <a>.
   * 2. Determine which ancestor <li>s need to be expanded.
   * 3. Auto-expand those ancestors (unless already open).
   * 4. Auto-collapse any previously auto-expanded items that are no longer
   *    ancestors of the active link (but leave user-pinned items alone).
   */
  function handleActiveChange(tocRoot) {
    var activeLink = tocRoot.querySelector("a.md-nav__link--active");

    // Determine which nested <li>s must be open for the active link
    var requiredOpen = activeLink ? ancestorItems(activeLink) : [];

    // Also include the active link's own <li> if it is a nested item
    if (activeLink) {
      var parentLi = activeLink.closest("li.md-nav__item--nested");
      if (parentLi && requiredOpen.indexOf(parentLi) === -1) {
        requiredOpen.push(parentLi);
      }
    }

    var requiredSet = new Set(requiredOpen);

    // Collapse items that were auto-expanded last round but are no longer
    // needed — skip anything the user has pinned open.
    autoExpanded.forEach(function (li) {
      if (!requiredSet.has(li) && !userPinned.has(li)) {
        collapse(li);
      }
    });

    // Build the new auto-expanded set
    autoExpanded = new Set();

    requiredOpen.forEach(function (li) {
      if (li.classList.contains("toc-collapsed")) {
        expand(li);
      }
      // Track as auto-expanded only if not user-pinned
      if (!userPinned.has(li)) {
        autoExpanded.add(li);
      }
    });
  }

  // ── Desktop detection ────────────────────────────────────────────────
  //
  // This entire script is designed for the desktop persistent sidebar
  // (≥76.25em).  On mobile, Material uses a slide-out drawer with its
  // own drill-down navigation, scroll management, and back-button
  // behaviour.  Running our DOM restructuring on mobile breaks the
  // drawer's scroll container and makes it impossible to scroll past
  // the visible items.
  //
  // We use matchMedia to detect the breakpoint and only bootstrap on
  // desktop.  On resize/orientation changes that cross the breakpoint,
  // the page reloads via Material's SPA system anyway, so we don't need
  // a live resize listener.

  var desktopQuery = window.matchMedia("(min-width: 76.25em)");

  function isDesktop() {
    return desktopQuery.matches;
  }

  // ── Bootstrap ───────────────────────────────────────────────────────

  function bootstrap() {
    if (!isDesktop()) return;  // skip all DOM manipulation on mobile
    initCollapsibleToc();
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", bootstrap);
  } else {
    bootstrap();
  }

  // Re-init after instant navigation (mkdocs-material SPA mode)
  if (typeof document$ !== "undefined") {
    document$.subscribe(function () {
      bootstrap();
    });
  }
})();
