/* sidenotes.js — Collision avoidance and hover linking for sidenotes.
 *
 * HTML structure produced by Filters/Sidenotes.hs:
 *   <sup class="sidenote-ref" id="snref-N"><a href="#sn-N">N</a></sup>
 *   <span class="sidenote" id="sn-N">…</span>
 *
 * #markdownBody must be position: relative (layout.css guarantees this).
 * The .sidenote spans are position: absolute; left: calc(100% + 2.5rem).
 * Without an explicit top they sit at their "hypothetical static position",
 * which is fine for isolated notes but causes overlaps when notes are close.
 *
 * This script:
 *   1. Anchors each sidenote's top to its reference's offsetTop so the
 *      alignment is explicit and stable across all browsers.
 *   2. Walks notes top-to-bottom and pushes each one below the previous
 *      if they would overlap (with a small gap between them).
 *   3. Wires bidirectional hover highlights between each ref↔sidenote pair.
 */

(function () {
    'use strict';

    const GAP = 12; /* minimum px gap between successive sidenotes */

    function positionSidenotes() {
        const body = document.getElementById('markdownBody');
        if (!body) return;

        const notes = Array.from(body.querySelectorAll('.sidenote'));
        if (notes.length === 0) return;

        /* Only run on wide viewports where sidenotes are visible. */
        if (getComputedStyle(notes[0]).display === 'none') return;

        let prevBottom = 0;

        notes.forEach(function (sn) {
            /* Reset any prior JS-applied top so offsetTop reads correctly. */
            sn.style.top = '';

            const id    = sn.id;                          /* "sn-N"    */
            const refId = 'snref-' + id.slice(3);         /* "snref-N" */
            const ref   = document.getElementById(refId);

            /* Preferred top: align with the reference superscript. */
            const preferred = ref ? ref.offsetTop : sn.offsetTop;
            const top       = Math.max(preferred, prevBottom + GAP);

            sn.style.top = top + 'px';
            prevBottom   = top + sn.offsetHeight;
        });
    }

    /* ------------------------------------------------------------------ */
    /*  Hover + click wiring                                               */
    /* ------------------------------------------------------------------ */

    /* At most one sidenote is "focused" (click-sticky) at a time. */
    let focusedPair = null;

    function wireHover(ref, sn) {
        let hovering = false;
        let focused  = false;

        function update() {
            const active = focused || hovering;
            ref.classList.toggle('is-active', active);
            sn.classList.toggle('is-active', active);
        }

        function unfocus() { focused = false; update(); }

        ref.addEventListener('mouseenter', function () { hovering = true;  update(); });
        ref.addEventListener('mouseleave', function () { hovering = false; update(); });
        sn.addEventListener('mouseenter',  function () { hovering = true;  update(); });
        sn.addEventListener('mouseleave',  function () { hovering = false; update(); });

        /* Click on the superscript link: sticky focus on wide viewports,
           normal anchor scroll on narrow viewports (sidenote hidden). */
        const link = ref.querySelector('a');
        if (link) {
            link.addEventListener('click', function (e) {
                e.preventDefault();
                if (getComputedStyle(sn).display === 'none') return; /* narrow: no sidenote to focus */
                if (focused) {
                    focused = false;
                    focusedPair = null;
                } else {
                    if (focusedPair) focusedPair.unfocus();
                    focused = true;
                    focusedPair = { ref: ref, sn: sn, unfocus: unfocus };
                }
                update();
            });
        }
    }

    /* Click anywhere outside a focused pair dismisses it. */
    document.addEventListener('click', function (e) {
        if (focusedPair &&
            !focusedPair.ref.contains(e.target) &&
            !focusedPair.sn.contains(e.target)) {
            focusedPair.unfocus();
            focusedPair = null;
        }
    });

    function init() {
        const body = document.getElementById('markdownBody');
        if (!body) return;

        body.querySelectorAll('.sidenote').forEach(function (sn) {
            const refId = 'snref-' + sn.id.slice(3);
            const ref   = document.getElementById(refId);
            if (ref) wireHover(ref, sn);
        });

        positionSidenotes();
    }

    document.addEventListener('DOMContentLoaded', init);
    window.addEventListener('resize', positionSidenotes);
}());
