/* =========================================================
   ZETTELKASTEN GARDEN  script.js
   ========================================================= */

document.addEventListener('DOMContentLoaded', () => {
  rearrangeDOM();
  buildSidebar();
  initNoteSystem();
  initSidebarRail();
  initBacklinkPanel();
  initLinkPreviews();
  initCopyButtons();
  initFootnoteSidenotes();
  initThemeToggle();
  initCountdowns();
  initTOCHighlighting();
  initMediaFixes(document);
  initReadingProgress();
  hookSearchResults();
});

/* =========================================================
   DOM REARRANGEMENT
   ========================================================= */

function rearrangeDOM() {
  const body      = document.body;
  const preamble  = document.getElementById('preamble');
  const stackRoot = document.getElementById('stack-root');
  const postamble = document.getElementById('postamble');

  if (!stackRoot) return;

  const content = stackRoot.querySelector('#content');

  // Inject breadcrumb trail into header
  const bannerHeader = preamble?.querySelector('.banner-header');
  if (bannerHeader) {
    const bannerLeft   = bannerHeader.querySelector('.banner-left');
    const bannerSearch = bannerHeader.querySelector('.banner-search');
    const crumbTrail   = el('div', { id: 'breadcrumb-trail', 'aria-label': 'Navigation history' });
    if (bannerLeft && bannerSearch) {
      bannerHeader.insertBefore(crumbTrail, bannerSearch);
    } else {
      bannerHeader.appendChild(crumbTrail);
    }
  }

  const layoutBody  = el('div', { id: 'layout-body' });
  const sidebar     = el('nav', { id: 'sidebar', 'aria-label': 'Navigation' });
  const main        = el('div', { id: 'main' });
  const noteToolbar = el('div', { id: 'note-toolbar' });
  const mainInner   = el('div', { id: 'main-inner' });
  const contentArea = el('div', { id: 'content-area' });
  const backlinkPnl = el('div', { id: 'backlink-panel', role: 'complementary', 'aria-label': 'Backlinks' });

  buildNoteToolbar(noteToolbar);

  backlinkPnl.innerHTML = `
    <div class="bp-header">
      <span class="bp-label">backlinks</span>
      <button class="bp-close" id="bp-close" title="Close backlinks" aria-label="Close backlinks">\u00D7</button>
    </div>
    <div class="bp-list" id="bp-list">
      <p class="bp-empty">No backlinks to this note.</p>
    </div>
  `;

  if (content) {
    const rootPane = el('div', { class: 'tab-content active', 'data-tab-id': 'tab-root' });
    rootPane.appendChild(content);
    contentArea.appendChild(rootPane);
  }

  mainInner.appendChild(contentArea);
  mainInner.appendChild(backlinkPnl);
  main.appendChild(noteToolbar);
  main.appendChild(mainInner);
  layoutBody.appendChild(sidebar);
  layoutBody.appendChild(main);
  stackRoot.remove();

  const progress = el('div', { id: 'reading-progress' });
  body.appendChild(progress);

  if (preamble?.nextSibling) {
    body.insertBefore(layoutBody, preamble.nextSibling);
  } else {
    body.appendChild(layoutBody);
  }
  if (postamble) body.appendChild(postamble);
}

function buildNoteToolbar(toolbar) {
  toolbar.innerHTML = `
    <button class="toolbar-btn" id="tb-back" title="Go back" aria-label="Go back">\u2190 back</button>
    <button class="toolbar-btn" id="tb-forward" title="Go forward" aria-label="Go forward">forward \u2192</button>
    <div class="toolbar-sep"></div>
    <button class="toolbar-btn" id="tb-toc" title="Toggle table of contents">contents</button>
    <div class="toolbar-spacer"></div>
    <span class="note-graph-pos" id="note-graph-pos"></span>
    <div class="toolbar-sep"></div>
    <button class="toolbar-btn" id="tb-backlinks" title="Toggle backlinks panel">backlinks</button>
  `;
}

/* =========================================================
   SIDEBAR RAIL
   ========================================================= */

function initSidebarRail() {
  const sidebar = document.getElementById('sidebar');
  if (!sidebar) return;

  const railBtns = [
    { label: '\u2630', title: 'All notes', id: 'rail-tree', panel: 'tree' },
    { label: '\u2315', title: 'Search',    id: 'rail-search', action: 'search' },
  ];

  railBtns.forEach(({ label, title, id, panel, action }) => {
    const btn = el('button', { class: 'rail-btn', id, title, 'aria-label': title });
    btn.textContent = label;
    sidebar.appendChild(btn);

    btn.addEventListener('click', () => {
      if (action === 'search') {
        document.getElementById('search-box')?.focus();
        return;
      }
      const isExpanded = sidebar.classList.contains('expanded');
      const samePanel  = sidebar.dataset.activePanel === panel;

      if (isExpanded && samePanel) {
        sidebar.classList.remove('expanded');
        sidebar.dataset.activePanel = '';
        btn.classList.remove('active');
      } else {
        sidebar.classList.add('expanded');
        sidebar.dataset.activePanel = panel;
        sidebar.querySelectorAll('.rail-btn').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        showSidebarPanel(panel);
      }
    });
  });

  // Mobile backdrop
  let backdrop = document.getElementById('sidebar-backdrop');
  if (!backdrop) {
    backdrop = el('div', { id: 'sidebar-backdrop' });
    document.body.appendChild(backdrop);
  }
  backdrop.addEventListener('click', () => {
    sidebar.classList.remove('expanded', 'mobile-open');
    backdrop.classList.remove('visible');
    sidebar.querySelectorAll('.rail-btn').forEach(b => b.classList.remove('active'));
  });

  initSidebarToggle();
}

function showSidebarPanel(panel) {
  const sidebar = document.getElementById('sidebar');
  sidebar.querySelectorAll('.sidebar-panel').forEach(p => p.remove());

  if (panel === 'tree') {
    const panelEl = el('div', { class: 'sidebar-panel' });
    panelEl.innerHTML = `
      <div class="sidebar-header">
        <span class="sidebar-header-label">all notes</span>
        <button class="sidebar-close-btn" aria-label="Close sidebar">\u00D7</button>
      </div>
      <div id="file-tree"></div>
    `;
    sidebar.appendChild(panelEl);

    panelEl.querySelector('.sidebar-close-btn').addEventListener('click', () => {
      sidebar.classList.remove('expanded');
      sidebar.dataset.activePanel = '';
      sidebar.querySelectorAll('.rail-btn').forEach(b => b.classList.remove('active'));
    });

    buildFileTree(panelEl.querySelector('#file-tree'));
  }
}

async function buildFileTree(container) {
  const currentPath = location.pathname;
  let treeData = null;

  try {
    const res = await fetch('/assets/sidebar-tree.json');
    if (res.ok) {
      const json = await res.json();
      if (Array.isArray(json.tree)) treeData = json.tree;
    }
  } catch (_) {}

  if (treeData) {
    renderTree(treeData, container, currentPath);
    return;
  }

  const groups = await buildGroupsFromSearchIndex();
  if (!groups?.length) {
    const empty = el('div', { class: 'tree-file' });
    empty.style.cssText = 'opacity:.4;font-size:.7rem;padding:8px 14px;';
    empty.textContent = 'no notes found';
    container.appendChild(empty);
    return;
  }
  const fallbackTree = groups.map(g => ({ label: g.label, children: [], files: g.files }));
  renderTree(fallbackTree, container, currentPath);
}

function renderTree(nodes, container, currentPath) {
  nodes.forEach(node => {
    const folderEl  = el('div', { class: 'tree-folder', 'data-folder': node.label });
    const icon      = el('span', { class: 'tree-icon' });
    icon.textContent = '\u25BE ';
    const labelSpan = el('span', { class: 'tree-folder-label' });
    labelSpan.textContent = node.label;
    folderEl.append(icon, labelSpan);

    const childrenWrap = el('div', { class: 'tree-children' });

    (node.files || []).forEach(f => {
      const pathname = normalisePathname(f.url);
      childrenWrap.appendChild(makeFileItem({ title: f.title, pathname }, currentPath));
    });

    if (node.children?.length) renderTree(node.children, childrenWrap, currentPath);

    folderEl.addEventListener('click', () => {
      const isOpen = childrenWrap.style.display !== 'none';
      childrenWrap.style.display = isOpen ? 'none' : '';
      icon.textContent = isOpen ? '\u25B8 ' : '\u25BE ';
    });

    container.appendChild(folderEl);
    container.appendChild(childrenWrap);
  });
}

async function buildGroupsFromSearchIndex() {
  try {
    const res = await fetch('/search-index.json');
    if (!res.ok) return null;
    const raw  = await res.json();
    const all  = Array.isArray(raw) ? raw : raw.entries || raw.notes || [];
    const files = all
      .filter(e => e && (e.url || e.path || e.pathname))
      .map(e => ({
        title: e.title || filenameToTitle(e.url || e.path || e.pathname),
        url:   e.url   || e.path || e.pathname,
      }))
      .sort((a, b) => a.title.localeCompare(b.title));
    return files.length ? [{ label: 'all notes', files }] : null;
  } catch (_) { return null; }
}

function makeFileItem({ title, pathname }, currentPath) {
  const fileEl = el('div', {
    class: 'tree-file' + (pathname === currentPath ? ' active' : ''),
    'data-pathname': pathname,
  });
  const iconSpan = el('span', { class: 'tree-icon' });
  iconSpan.textContent = '\u00B7 ';
  fileEl.appendChild(iconSpan);
  fileEl.appendChild(document.createTextNode(title));
  fileEl.title = title;
  fileEl.addEventListener('click', () => {
    document.getElementById('sidebar')?.classList.remove('expanded');
    openNote(pathname, title);
  });
  return fileEl;
}

/* =========================================================
   NOTE SYSTEM
   ========================================================= */

const noteHistory  = [];
let   historyIndex = -1;
let   activeNoteId = null;

function makeHistoryState(entry, hash = '') {
  return {
    notePathname: entry.pathname,
    noteLabel: entry.label,
    noteHash: hash || '',
    isRoot: !!entry.isRoot,
  };
}

function historyUrlFor(entry, hash = '') {
  if (entry.isRoot) {
    return location.pathname + location.search;
  }
  return entry.pathname + (hash || '');
}

function readHashState() {
  const hash  = location.hash.slice(1);
  const state = { active: null };
  if (!hash) return state;
  hash.split('&').forEach(part => {
    const [key, val] = part.split('=');
    if (!val) return;
    if (key === 'active') state.active = decodeURIComponent(val);
  });
  return state;
}

function writeHashState() {
  const current = noteHistory[historyIndex];
  if (!current || current.isRoot) {
    history.replaceState(null, '', location.pathname + location.search);
    return;
  }
    const newHash = '#active=' + encodeURIComponent(current.pathname);
  history.replaceState(null, '', location.pathname + location.search + newHash);
}

function initNoteSystem() {
  const rootPane = document.querySelector(".tab-content[data-tab-id='tab-root']");
  if (!rootPane) return;

  const titleEl = rootPane.querySelector('.title-section .title, h1.title, h1');
  const label   = titleEl?.textContent.trim() || document.title || 'Home';

  const rootEntry = { id: 'tab-root', label, pathname: location.pathname, paneEl: rootPane, isRoot: true };
  noteHistory.push(rootEntry);
  historyIndex = 0;
  activeNoteId = 'tab-root';

updateBreadcrumb();
updateToolbar();
initMediaFixes(rootPane);
if (window._gallerySetupVideo) window._gallerySetupVideo(rootPane);
if (window._gallerySetupAudio) window._gallerySetupAudio(rootPane);

// Register the initial entry with browser history
history.replaceState(
  makeHistoryState(rootEntry),
  '',
  historyUrlFor(rootEntry)
);
  
  // Intercept internal .html link clicks only
  document.addEventListener('click', e => {
    const link = e.target.closest('a');
    if (!link) return;
    const href = link.getAttribute('href');
    if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return;
    let url;
    try { url = new URL(href, location.href); } catch { return; }
      if (url.origin !== location.origin) return;
    // Only intercept .html pages — let assets, PDFs, etc. through
      if (!url.pathname.endsWith('.html')) return;

      e.preventDefault();

      const pathname = normalisePathname(url.pathname);
      const hash     = url.hash || '';

      const label = link.textContent.trim() || filenameToTitle(pathname);

      openNote(pathname, label, hash);
  });

  // Toolbar buttons
  document.getElementById('tb-back')?.addEventListener('click', goBack);
  document.getElementById('tb-forward')?.addEventListener('click', goForward);
  document.getElementById('tb-backlinks')?.addEventListener('click', toggleBacklinkPanel);
  document.getElementById('tb-toc')?.addEventListener('click', toggleTOC);

  // Backlink panel close
  document.getElementById('bp-close')?.addEventListener('click', () => {
    document.getElementById('backlink-panel')?.classList.remove('open');
    document.getElementById('tb-backlinks')?.classList.remove('active');
  });

window.addEventListener('popstate', async (e) => {
  const state = e.state;

  // No state: fall back to root
  if (!state || !state.notePathname) {
    if (noteHistory[0]) activateNote(noteHistory[0].id, true);
    return;
  }

  const pathname = normalisePathname(state.notePathname);
  const label = state.noteLabel || filenameToTitle(pathname);
  const hash = state.noteHash || '';

  const existing = noteHistory.find(n => n.pathname === pathname);

  if (existing) {
    activateNote(existing.id, true);
    if (hash) scrollToHash(existing.paneEl, hash);
    return;
  }

  await openNote(pathname, label, hash, true, false);
});
  
}

async function openNote(pathname, label, hash = '', silent = false, pushBrowserState = true) {
    console.log('Opening note:', pathname, { label, hash, silent });
    // Normalise
  pathname = normalisePathname(pathname);

  // Already at this note?
  const current = noteHistory[historyIndex];
  if (current?.pathname === pathname) {
    if (hash) scrollToHash(current.paneEl, hash);
    return;
  }

  // Already exists in history?
  const existing = noteHistory.find(n => n.pathname === pathname);
if (existing) {
  historyIndex = noteHistory.indexOf(existing);
  activateNote(existing.id, true);
  if (hash) scrollToHash(existing.paneEl, hash);

  if (pushBrowserState) {
    history.pushState(
      makeHistoryState(existing, hash),
      '',
      historyUrlFor(existing, hash)
    );
  }
  return;
}

  // Show loading indicator
  const contentArea = document.getElementById('content-area');
  const loadingPane = el('div', { class: 'tab-content active', 'data-loading': '1' });
  loadingPane.innerHTML = '<div class="note-loading">Loading\u2026</div>';
  contentArea?.appendChild(loadingPane);
  // Hide all other panes while loading
  noteHistory.forEach(n => n.paneEl.classList.remove('active'));

  let contentEl;
  let resolvedLabel = label || filenameToTitle(pathname);

  try {
    const res = await fetch(pathname);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const doc = new DOMParser().parseFromString(await res.text(), 'text/html');

    const titleEl = doc.querySelector('.title-section .title, h1.title, h1');
    if (titleEl) resolvedLabel = titleEl.textContent.trim();

    // Prefer #content, else fall back gracefully
    contentEl = doc.getElementById('content')
              || doc.querySelector('.stack-pane')
              || (() => {
                  const d = document.createElement('div');
                  d.id = 'content';
                  d.innerHTML = '<p>Note loaded but content element not found.</p>';
                  return d;
                })();

    // Decode mermaid entities
    contentEl.querySelectorAll('.mermaid').forEach(m => {
      m.innerHTML = m.innerHTML
        .replace(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&amp;/g, '&');
    });

    fixRelativeSrcs(contentEl, pathname);

  } catch (err) {
    console.error('Note fetch error:', err);
    contentEl = document.createElement('div');
    contentEl.id = 'content';
    contentEl.innerHTML =
      `<p style="color:var(--fg-faint);padding:2rem 0">` +
      `Could not load <code>${escHtml(pathname)}</code>: ${escHtml(err.message)}</p>`;
  }

  // Remove the temporary loading pane
  loadingPane.remove();

  const id     = 'tab-' + Date.now();
  const paneEl = el('div', { class: 'tab-content', 'data-tab-id': id });
  paneEl.appendChild(contentEl);

  contentArea?.appendChild(paneEl);

  const entry = { id, label: resolvedLabel, pathname, paneEl, isRoot: false };

  // Truncate forward history when branching
  if (historyIndex < noteHistory.length - 1) {
    noteHistory.slice(historyIndex + 1).forEach(n => {
      if (!n.isRoot) n.paneEl.remove();
    });
    noteHistory.splice(historyIndex + 1);
  }

  noteHistory.push(entry);
  historyIndex = noteHistory.length - 1;

activateNote(id, true);
if (hash) scrollToHash(paneEl, hash);

if (pushBrowserState) {
  history.pushState(
    makeHistoryState(entry, hash),
    '',
    historyUrlFor(entry, hash)
  );
}
  // Run mermaid on newly loaded diagrams
  if (typeof mermaid !== 'undefined') {
    const diagrams = paneEl.querySelectorAll('.mermaid:not([data-processed])');
    if (diagrams.length) mermaid.run({ nodes: diagrams });
  }

  initCopyButtonsIn(paneEl);
  initFootnoteSidenotesIn(paneEl);
  initMediaFixes(paneEl);
  initLinkPreviewsIn(paneEl);

  const trySetup = () => {
    if (window._gallerySetupVideo) window._gallerySetupVideo(paneEl);
    if (window._gallerySetupAudio) window._gallerySetupAudio(paneEl);
  };
  if (window._gallerySetupVideo) { trySetup(); } else { setTimeout(trySetup, 200); }

  updateSidebarActive(pathname);
}

function activateNote(id, skipBrowserWrite = true) {
  activeNoteId = id;
  const idx = noteHistory.findIndex(n => n.id === id);
  if (idx !== -1) historyIndex = idx;

  noteHistory.forEach(n => n.paneEl.classList.toggle('active', n.id === id));

  const area = document.getElementById('content-area');
  if (area) area.scrollTop = 0;

  updateBreadcrumb();
  updateToolbar();

  // Refresh backlink panel if open
  const panel = document.getElementById('backlink-panel');
  if (panel?.classList.contains('open')) refreshBacklinkPanel();

  updateSidebarActive(noteHistory[historyIndex]?.pathname);

}

function goBack() {
  window.history.back();
}

function goForward() {
  window.history.forward();
}

function updateBreadcrumb() {
  const trail = document.getElementById('breadcrumb-trail');
  if (!trail) return;
  trail.innerHTML = '';

  const start = Math.max(0, historyIndex - 2);
  const slice = noteHistory.slice(start, historyIndex + 1);

  slice.forEach((entry, i) => {
    if (i > 0) {
      const sep = el('span', { class: 'crumb-sep', 'aria-hidden': 'true' });
      sep.textContent = '/';
      trail.appendChild(sep);
    }

    const isActive = i === slice.length - 1;
    const crumb = el('span', { class: 'crumb' + (isActive ? ' active' : '') });
    if (isActive) crumb.setAttribute('aria-current', 'page');
    crumb.textContent = entry.label;
    crumb.title = entry.pathname;

    if (!isActive) {
      const targetIdx = start + i;
      crumb.addEventListener('click', () => {
        historyIndex = targetIdx;
        activateNote(entry.id);
      });
    }

    trail.appendChild(crumb);
  });
}

function updateToolbar() {
  const backBtn = document.getElementById('tb-back');
  if (backBtn) {
    const canGoBack = historyIndex > 0;
    backBtn.disabled = !canGoBack;
    backBtn.style.opacity = canGoBack ? '1' : '0.35';
  }

  const fwdBtn = document.getElementById('tb-forward');
  if (fwdBtn) {
    const canGoFwd = historyIndex < noteHistory.length - 1;
    fwdBtn.disabled = !canGoFwd;
    fwdBtn.style.opacity = canGoFwd ? '1' : '0.35';
  }

  const pos = document.getElementById('note-graph-pos');
  if (pos) {
    const current = noteHistory[historyIndex];
    if (current) {
      const name = current.pathname.split('/').pop().replace('.html', '');
      // Strip the timestamp prefix from Org-Roam filenames
      const clean = name.replace(/^\d{14}-/, '');
      pos.textContent = clean.length > 24 ? clean.slice(0, 22) + '\u2026' : clean;
    }
  }
}

function toggleTOC() {
  const active = noteHistory[historyIndex];
  if (!active) return;
  const toc = active.paneEl.querySelector('#table-of-contents');
  if (!toc) return;
  const hidden = toc.style.display === 'none';
  toc.style.display = hidden ? '' : 'none';
  document.getElementById('tb-toc')?.classList.toggle('active', hidden);
}

/* =========================================================
   BACKLINK PANEL
   ========================================================= */

function initBacklinkPanel() {
  // Panel built in rearrangeDOM; nothing else needed here
}

function toggleBacklinkPanel() {
  const panel = document.getElementById('backlink-panel');
  const btn   = document.getElementById('tb-backlinks');
  if (!panel) return;
  const isOpen = panel.classList.toggle('open');
  btn?.classList.toggle('active', isOpen);
  if (isOpen) refreshBacklinkPanel();
}

async function refreshBacklinkPanel() {
  const panel   = document.getElementById('backlink-panel');
  if (!panel?.classList.contains('open')) return;

  const list    = document.getElementById('bp-list');
  const countEl = document.getElementById('bp-count');
  const current = noteHistory[historyIndex];
  if (!list || !current) return;

  list.innerHTML = '<p class="bp-empty" style="opacity:.5;font-style:italic">Loading\u2026</p>';
  if (countEl) countEl.textContent = '\u2026';

  try {
    const res = await fetch('/search-index.json');
    if (!res.ok) throw new Error('no index');
    const raw  = await res.json();
    const all  = Array.isArray(raw) ? raw : raw.entries || raw.notes || [];

    // Match by both the bare filename and the full pathname
    const currentFile = current.pathname.split('/').pop().replace(/\.html$/, '');
	  const currentPath = normalisePathname(decodeURIComponent(current.pathname));

	  const backlinks = all.filter(e => {
		  if (!e) return false;

		  const entryUrl = normalisePathname(decodeURIComponent(e.url || ''));
		  if (entryUrl === currentPath) return false;

		  const links = (e.links || []).map(l =>
			  normalisePathname(decodeURIComponent(l))
		  );

		  return links.some(l => l === currentPath);
	  });      

      if (countEl) countEl.textContent = String(backlinks.length);

    if (!backlinks.length) {
      list.innerHTML = '<p class="bp-empty">No backlinks found for this note.</p>';
      return;
    }

	  console.log("Have backlinks for",{
		  currentPath,
		  currentFile,
		  sampleEntry: all[0]
	  });

    list.innerHTML = '';
    backlinks.forEach(bl => {
      const pathname = normalisePathname(bl.url || bl.path || bl.pathname || '');
      const title    = bl.title || filenameToTitle(pathname);
      const excerpt  = (bl.content || bl.body || bl.text || '').slice(0, 140).trim();

      const item = el('div', { class: 'bp-item' });

      const titleDiv = el('div', { class: 'bp-item-title' });
      titleDiv.textContent = title;
      item.appendChild(titleDiv);

      if (excerpt) {
        const excDiv = el('div', { class: 'bp-item-excerpt' });
        excDiv.textContent = excerpt + '\u2026';
        item.appendChild(excDiv);
      }

      item.addEventListener('click', () => openNote(pathname, title));
      list.appendChild(item);
    });

  } catch (_) {
    list.innerHTML = '<p class="bp-empty">Could not load backlinks.</p>';
    if (countEl) countEl.textContent = '!';
  }
}

/* =========================================================
   LINK HOVER PREVIEWS
   ========================================================= */

let previewTimer = null;
const previewCache = {};

function initLinkPreviews() {
  if (!document.getElementById('link-preview')) {
    const card = el('div', { id: 'link-preview', role: 'tooltip', 'aria-hidden': 'true' });
    card.innerHTML = `
      <div class="preview-inner">
        <div class="preview-title" id="preview-title">Note title</div>
        <div class="preview-body" id="preview-body">Loading\u2026</div>
      </div>
      <div class="preview-footer">click to open</div>
    `;
    document.body.appendChild(card);
  }
  initLinkPreviewsIn(document);
}

function initLinkPreviewsIn(root) {
  // Only attach previews to internal .html links, not external or asset links
  root.querySelectorAll('a[href*=".html"]').forEach(link => {
    if (link.dataset.previewInited) return;
    // Skip links to external origins
    try {
      const url = new URL(link.getAttribute('href'), location.href);
      if (url.origin !== location.origin) return;
    } catch { return; }

    link.dataset.previewInited = '1';

    link.addEventListener('mouseenter', e => {
      clearTimeout(previewTimer);
      previewTimer = setTimeout(() => showPreview(link, e), 300);
    });
    link.addEventListener('mouseleave', () => {
      clearTimeout(previewTimer);
      hidePreview();
    });
    link.addEventListener('mousemove', e => {
      const card = document.getElementById('link-preview');
      if (card?.classList.contains('visible')) positionPreview(card, e);
    });
  });
}

async function showPreview(link, evt) {
  const card = document.getElementById('link-preview');
  if (!card) return;

  const href = link.getAttribute('href');
  let url;
  try { url = new URL(href, location.href); } catch { return; }
  if (url.origin !== location.origin) return;

  const pathname = normalisePathname(url.pathname);
  const titleEl  = document.getElementById('preview-title');
  const bodyEl   = document.getElementById('preview-body');

  titleEl.textContent = link.textContent.trim() || filenameToTitle(pathname);
  bodyEl.textContent  = 'Loading\u2026';

  positionPreview(card, evt);
  card.classList.add('visible');
  card.setAttribute('aria-hidden', 'false');

  if (previewCache[pathname]) {
    const { title, body } = previewCache[pathname];
    titleEl.textContent = title;
    bodyEl.textContent  = body || 'No preview available.';
    return;
  }

  try {
    const res = await fetch(pathname);
    if (!res.ok) throw new Error('fetch failed');
    const doc    = new DOMParser().parseFromString(await res.text(), 'text/html');
    const h1     = doc.querySelector('.title-section .title, h1.title, h1');
    const paras  = [...doc.querySelectorAll('#content p')];
    const title  = h1?.textContent.trim() || filenameToTitle(pathname);
    const body   = paras.slice(0, 3).map(p => p.textContent.trim()).join(' ').slice(0, 280);

    previewCache[pathname] = { title, body };

    if (card.classList.contains('visible')) {
      titleEl.textContent = title;
      bodyEl.textContent  = body || 'No preview available.';
    }
  } catch (_) {
    if (card.classList.contains('visible')) bodyEl.textContent = 'Preview unavailable.';
  }
}

function positionPreview(card, evt) {
  const margin = 14;
  const cw = card.offsetWidth  || 300;
  const ch = card.offsetHeight || 200;
  let x = evt.clientX + margin;
  let y = evt.clientY + margin;
  if (x + cw > window.innerWidth  - margin) x = evt.clientX - cw - margin;
  if (y + ch > window.innerHeight - margin) y = evt.clientY - ch - margin;
  card.style.left = x + 'px';
  card.style.top  = y + 'px';
}

function hidePreview() {
  const card = document.getElementById('link-preview');
  card?.classList.remove('visible');
  card?.setAttribute('aria-hidden', 'true');
}

/* =========================================================
   READING PROGRESS BAR
   ========================================================= */

function initReadingProgress() {
  const bar  = document.getElementById('reading-progress');
  const area = document.getElementById('content-area');
  if (!bar || !area) return;

  area.addEventListener('scroll', () => {
    const { scrollTop, scrollHeight, clientHeight } = area;
    const pct = scrollHeight <= clientHeight
      ? 0
      : (scrollTop / (scrollHeight - clientHeight)) * 100;
    bar.style.width = pct + '%';
  }, { passive: true });
}

/* =========================================================
   SEARCH RESULTS HOOK
   ========================================================= */

function hookSearchResults() {
  const resultsBox = document.getElementById('search-results');
  const searchBox  = document.getElementById('search-box');
  if (!resultsBox) return;

  resultsBox.addEventListener('mousedown', e => {
    let item = e.target;
    while (item && item !== resultsBox) {
      const url =
        item.dataset.url   ||
        item.dataset.href  ||
        item.dataset.path  ||
        item.querySelector?.('a')?.getAttribute('href') ||
        (item.tagName === 'A' ? item.getAttribute('href') : null);

      if (url) {
        e.preventDefault();
        e.stopPropagation();
        resultsBox.innerHTML = '';
        if (searchBox) searchBox.value = '';

        let resolved;
        try { resolved = new URL(url, location.href); } catch { return; }
        if (resolved.origin !== location.origin) { window.open(url, '_blank'); return; }
        if (!resolved.pathname.endsWith('.html')) return;

        const label = item.textContent.trim() || filenameToTitle(resolved.pathname);
        openNote(resolved.pathname, label, resolved.hash);
        return;
      }
      item = item.parentElement;
    }
  });

  if (searchBox) {
    searchBox.addEventListener('keydown', e => {
      if (e.key !== 'Enter') return;
      const active = resultsBox.querySelector('.active, li:first-child, div:first-child');
      if (!active) return;
      const url =
        active.dataset.url  || active.dataset.href ||
        active.querySelector?.('a')?.getAttribute('href');
      if (!url) return;
      resultsBox.innerHTML = '';
      searchBox.value = '';
      let resolved;
      try { resolved = new URL(url, location.href); } catch { return; }
      openNote(resolved.pathname, active.textContent.trim(), resolved.hash);
    });
  }
}

/* =========================================================
   SIDEBAR TOGGLE
   ========================================================= */

function initSidebarToggle() {
  const sidebar = document.getElementById('sidebar');
  if (!sidebar) return;

  // Keyboard shortcut: Ctrl+\ toggles sidebar
  document.addEventListener('keydown', e => {
    if ((e.ctrlKey || e.metaKey) && e.key === '\\') {
      e.preventDefault();
      const nowExpanded = sidebar.classList.toggle('expanded');
      if (nowExpanded && !sidebar.dataset.activePanel) {
        sidebar.dataset.activePanel = 'tree';
        showSidebarPanel('tree');
        sidebar.querySelector('#rail-tree')?.classList.add('active');
      }
      if (!nowExpanded) {
        sidebar.dataset.activePanel = '';
        sidebar.querySelectorAll('.rail-btn').forEach(b => b.classList.remove('active'));
      }
    }
  });

  // Touch swipe-to-open
  let touchStartX = null;
  document.addEventListener('touchstart', e => {
    if (e.touches[0].clientX < 20) touchStartX = e.touches[0].clientX;
  }, { passive: true });
  document.addEventListener('touchend', e => {
    if (touchStartX === null) return;
    const dx = e.changedTouches[0].clientX - touchStartX;
    touchStartX = null;
    if (dx > 40) {
      sidebar.classList.add('expanded', 'mobile-open');
      document.getElementById('sidebar-backdrop')?.classList.add('visible');
    }
  }, { passive: true });

  sidebar.addEventListener('touchstart', e => {
    touchStartX = e.touches[0].clientX;
  }, { passive: true });
  sidebar.addEventListener('touchend', e => {
    if (touchStartX === null) return;
    const dx = e.changedTouches[0].clientX - touchStartX;
    touchStartX = null;
    if (dx < -40) {
      sidebar.classList.remove('expanded', 'mobile-open');
      document.getElementById('sidebar-backdrop')?.classList.remove('visible');
    }
  }, { passive: true });
}

/* =========================================================
   SIDEBAR ACTIVE STATE
   ========================================================= */

function updateSidebarActive(pathname) {
  document.querySelectorAll('.tree-file').forEach(f => {
    f.classList.toggle('active', f.dataset.pathname === pathname);
  });
}

/* =========================================================
   MEDIA FIXES
   ========================================================= */

function initMediaFixes(root) {
  root.querySelectorAll('audio').forEach(audio => {
    audio.controls = true;
    if (!audio.closest('.audio-player')) {
      const wrapper   = document.createElement('div');
      wrapper.className = 'audio-player';
      const src       = audio.src || audio.querySelector?.('source')?.src || '';
      const labelText = audio.dataset.label || audio.title
        || src.split('/').pop().replace(/\.[^.]+$/, '') || 'audio';
      const labelEl   = document.createElement('div');
      labelEl.className   = 'audio-label';
      labelEl.textContent = labelText;
      audio.parentNode.insertBefore(wrapper, audio);
      wrapper.append(labelEl, audio);
    }
  });

  root.querySelectorAll('video').forEach(video => {
    video.controls = true;
    video.style.cssText = 'max-width:100%;width:100%;height:auto;display:block;border-radius:6px;';
    if (!video.closest('.video-container')) {
      const wrapper = document.createElement('div');
      wrapper.className = 'video-container';
      video.parentNode.insertBefore(wrapper, video);
      wrapper.appendChild(video);
    }
    if (!video.currentSrc) video.load();
  });
}

function fixRelativeSrcs(root, fromPathname) {
  const base = new URL(fromPathname, location.href).href.split('/').slice(0, -1).join('/') + '/';
  const fix = (elmt, attr) => {
    const val = elmt.getAttribute(attr);
    if (!val || val.startsWith('http') || val.startsWith('//')
        || val.startsWith('data:') || val.startsWith('#') || val.startsWith('/')) return;
    try { elmt.setAttribute(attr, new URL(val, base).href); } catch {}
  };
  root.querySelectorAll('img').forEach(e    => fix(e, 'src'));
  root.querySelectorAll('video').forEach(e  => fix(e, 'src'));
  root.querySelectorAll('audio').forEach(e  => fix(e, 'src'));
  root.querySelectorAll('source').forEach(e => fix(e, 'src'));
  root.querySelectorAll('a').forEach(e      => fix(e, 'href'));
}

/* =========================================================
   COPY BUTTONS
   ========================================================= */

function initCopyButtons()       { initCopyButtonsIn(document); }

function initCopyButtonsIn(root) {
  root.querySelectorAll('pre.src').forEach(block => {
    if (block.querySelector('.copy-btn')) return;
    block.style.position = 'relative';
    const btn = document.createElement('button');
    btn.className   = 'copy-btn';
    btn.textContent = 'copy';
    block.appendChild(btn);
    btn.addEventListener('click', async () => {
      const text = block.innerText.replace(btn.innerText, '').trim();
      try {
        await navigator.clipboard.writeText(text);
        btn.textContent = 'copied';
      } catch {
        btn.textContent = 'failed';
      }
      setTimeout(() => { btn.textContent = 'copy'; }, 1600);
    });
  });
}

/* =========================================================
   FOOTNOTE SIDENOTES
   ========================================================= */

function initFootnoteSidenotes()       { initFootnoteSidenotesIn(document); }

function initFootnoteSidenotesIn(root) {
  root.querySelectorAll('a.footref[href^="#fn"]').forEach(ref => {
    const sup = ref.closest('sup') || ref;
    if (sup.nextElementSibling?.classList.contains('footnote-sidenote')) return;

    const targetId = ref.getAttribute('href').slice(1);
    const anchor   = document.getElementById(targetId)
                   || root.querySelector?.(`#${CSS.escape(targetId)}`);
    if (!anchor) return;

    const footdef = anchor.closest('.footdef') || anchor.parentElement;
    if (!footdef) return;

    let paras = footdef.querySelectorAll('p.footpara');
    if (!paras.length) paras = footdef.querySelectorAll('.footpara');

    let parts = [];
    if (paras.length) {
      const seen = new Set();
      parts = [...paras].map(p => {
        const txt = p.textContent.trim().replace(/\s+/g, ' ');
        if (seen.has(txt)) return '';
        seen.add(txt);
        return p.innerHTML.trim();
      }).filter(Boolean);
    }
    if (!parts.length) {
      const clone = footdef.cloneNode(true);
      clone.querySelectorAll('sup.footnum, a[role="doc-backlink"]').forEach(n => n.remove());
      parts = [clone.innerHTML.trim()];
    }

    const sidenote = document.createElement('span');
    sidenote.className  = 'sidenote footnote-sidenote';
    sidenote.dataset.fn = ref.textContent.trim();
    sidenote.innerHTML  = parts.join(' ');
    sup.insertAdjacentElement('afterend', sidenote);
  });
}

/* =========================================================
   THEME TOGGLE
   ========================================================= */

function initThemeToggle() {
  const root    = document.documentElement;
  const key     = 'theme';
  const saved   = localStorage.getItem(key);
  const initial = (saved === 'dark' || saved === 'light') ? saved : 'dark';
  root.setAttribute('data-theme', initial);

  const btn = document.getElementById('theme-toggle');
  if (!btn) return;

  const updateIcon = theme => {
    // Moon for dark mode, sun for light mode
    btn.textContent = theme === 'dark' ? '\u263D' : '\u2600';
    btn.setAttribute('aria-label',
      theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme');
  };
  updateIcon(initial);

  btn.addEventListener('click', () => {
    const next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
    root.setAttribute('data-theme', next);
    localStorage.setItem(key, next);
    updateIcon(next);
  });
}

/* =========================================================
   COUNTDOWNS
   ========================================================= */

function initCountdowns() {
  const els = document.querySelectorAll('time.countdown');
  if (!els.length) return;
  const plural = (n, w) => `${n} ${w}${n === 1 ? '' : 's'}`;
  const render = el => {
    const target = new Date(el.getAttribute('datetime'));
    const label  = el.dataset.label || '';
    if (isNaN(target)) { el.textContent = ''; return; }
    let diff = target - new Date();
    if (diff <= 0) {
      el.textContent = `${label ? label + ' ' : ''}today`;
      el.classList.add('expired');
      return;
    }
    const d = Math.floor(diff / 86400000); diff %= 86400000;
    const h = Math.floor(diff / 3600000);  diff %= 3600000;
    const m = Math.floor(diff / 60000);    diff %= 60000;
    const s = Math.floor(diff / 1000);
    const parts = [];
    if (d) parts.push(plural(d, 'day'));
    parts.push(`${h}h ${m}m ${s}s`);
    el.textContent = `${label ? label + ' in: ' : ''}${parts.join(' ')}`;
  };
  const tick = () => els.forEach(render);
  tick();
  setInterval(tick, 1000);
}

/* =========================================================
   TOC HIGHLIGHTING
   ========================================================= */

function initTOCHighlighting() {
  // Scoped to the currently active pane
  const getActiveTOC = () => {
    const active = noteHistory[historyIndex];
    if (!active) return null;
    return active.paneEl.querySelector('#text-table-of-contents');
  };

  const toc = getActiveTOC() || document.querySelector('#text-table-of-contents');
  if (!toc) return;

  const links = [...toc.querySelectorAll('a[href^="#"]')];
  if (!links.length) return;

  const linkById = new Map();
  links.forEach(a => {
    const id = decodeURIComponent(a.hash.slice(1));
    const elmt = document.getElementById(id);
    if (elmt) linkById.set(id, a);
  });

  const headings = [...document.querySelectorAll('h2[id], h3[id], h4[id]')]
    .filter(h => linkById.has(h.id));

  const setActive = id => {
    links.forEach(a => {
      const active = a.hash === `#${id}`;
      a.classList.toggle('is-active', active);
      if (active) a.setAttribute('aria-current', 'true');
      else a.removeAttribute('aria-current');
    });
  };

  const headerOffset = 6.5 * parseFloat(getComputedStyle(document.documentElement).fontSize);
  const visible = new Map();

  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      const id = entry.target.id;
      if (entry.isIntersecting) {
        visible.set(id, entry.target.getBoundingClientRect().top - headerOffset);
      } else {
        visible.delete(id);
      }
    });
    if (visible.size) {
      const [id] = [...visible.entries()].sort((a, b) => Math.abs(a[1]) - Math.abs(b[1]))[0];
      setActive(id);
    }
  }, { rootMargin: `-${headerOffset}px 0px -70% 0px`, threshold: [0, 0.01, 0.1] });

  headings.forEach(h => observer.observe(h));

  toc.addEventListener('click', e => {
    const a = e.target.closest('a[href^="#"]');
    if (!a) return;
    const id   = decodeURIComponent(a.hash.slice(1));
    const elmt = document.getElementById(id);
    if (!elmt) return;
    e.preventDefault();
    elmt.scrollIntoView({ behavior: 'smooth', block: 'start' });
    elmt.setAttribute('tabindex', '-1');
    elmt.focus({ preventScroll: true });
    history.pushState(null, '', `#${id}`);
  });
}

/* =========================================================
   HELPERS
   ========================================================= */

function scrollToHash(paneEl, hash) {
  if (!hash) return;
  requestAnimationFrame(() => {
    const id     = hash.replace(/^#/, '');
    const target = paneEl.querySelector(`#${CSS.escape(id)}`);
    const area   = document.getElementById('content-area');
    if (target && area) area.scrollTop = target.offsetTop;
  });
}

function el(tag, attrs = {}) {
  const e = document.createElement(tag);
  Object.entries(attrs).forEach(([k, v]) => {
    if (v !== undefined) e.setAttribute(k, v);
  });
  return e;
}

function escHtml(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

function filenameToTitle(pathname) {
  const base  = (pathname || '').split('/').pop().replace(/\.html$/, '');
  // Strip Org-Roam timestamp prefix (14 digits)
  const clean = base.replace(/^\d{14}-/, '');
  return clean.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}

function normalisePathname(p) {
  if (!p) return '';
  if (p.startsWith('/')) return p;
  try { return new URL(p, location.href).pathname; } catch { return '/' + p; }
}

// Lazy sidebar builder stub (built on demand)
async function buildSidebar() {}

// Pane-out animation keyframe
const _s = document.createElement('style');
_s.textContent = `
  @keyframes pane-out {
    from { opacity:1; transform:translateX(0); }
    to   { opacity:0; transform:translateX(16px); }
  }
  .note-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 40vh;
    color: var(--fg-faint);
    font-family: var(--font-mono);
    font-size: 0.85rem;
    letter-spacing: 0.06em;
    opacity: 0.6;
  }
`;
document.head.appendChild(_s);
