<script setup lang="ts">
import type { MarkdownHeading } from 'astro';
import { onBeforeUnmount, onMounted, ref } from 'vue';

const props = defineProps<{ headings: MarkdownHeading[] }>();

// headings deeper than h3 don't display well because they are too deeply indented
const headings = props.headings.filter(h => h.depth <= 3);

// for each heading slug, track whether the corresponding heading is above the cutoff point
// (the cutoff point being a hypothetical line 2/3 of the way up the viewport)
let headingStatuses = Object.fromEntries(headings.map(h => ([h.slug, false])));
// we need to store a reference to the observer so we can dispose of it on resize/unmount
let headingObserver: IntersectionObserver | null = null;
// the final slug that should be highlighted as "current" in the TOC
let currentSlug = ref('');

function handleIntersectionUpdate(entries: IntersectionObserverEntry[], headingElems: HTMLElement[]) {
    for (const entry of entries) {
        const slug = entry.target.id;
        const status = entry.isIntersecting;
        headingStatuses[slug] = status;
    }

    // headings are in DOM order, so this gives us the last heading that's still above the cutoff point
    for (const elem of headingElems) {
        if (headingStatuses[elem.id]) {
            currentSlug.value = elem.id;
        }
        else {
            break;
        }
    }
}

function setupObserver() {
    // if there was already an observer, turn it off
    if (headingObserver) {
        headingObserver.disconnect();
    }
    const headingElems = headings.map(h => document.getElementById(h.slug)!);

    const obs = new IntersectionObserver(
        entries => handleIntersectionUpdate(entries, headingElems),
        // top margin equal to body height means that the intersection zone extends up beyond
        // the top of the document, i.e. elements can only enter/leave the zone at the bottom
        { rootMargin: `${document.body.clientHeight}px 0px -66% 0px` },
    );

    for (const elem of headingElems) {
        obs.observe(elem);
    }

    headingObserver = obs;
}

onMounted(() => {
    // create the observer once on component startup
    setupObserver();
    // any time the window resizes, the document height could change, so we need to recreate it
    window.addEventListener('resize', setupObserver);
});
onBeforeUnmount(() => {
    window.removeEventListener('resize', setupObserver);
    headingObserver?.disconnect();
});

</script>

<template>
    <div id="toc">
        <h5>C<span class="lower">ontents</span></h5>
        <ul id="toc-list">
            <li
                v-for="heading in headings"
                :data-current="heading.slug == currentSlug"
                :style="`--depth: ${heading.depth}`"
            >
                <span v-show="heading.slug == currentSlug" class="marker"></span>
                <a :href="`#${heading.slug}`">
                    {{ heading.text }}
                </a>
            </li>
        </ul>
    </div>
</template>

<style scoped>
#toc {
    position: sticky;
    top: 1.5rem;
    margin-left: 1rem;
    margin-right: 4rem;
    max-width: 18rem;

    font-size: var(--content-size-sm);
    color: var(--content-color-faded);

    /*
    minimum desirable TOC width is 8rem
    add 4rem for margins, giving total gutter width of 12.5rem
    multiply by 2 since there are two equally-sized gutters, then add content-width (52.5rem)
    */
    @media(max-width: 77.5rem) {
        display: none;
    }
}

h5 {
    font-variant: petite-caps;
    font-weight: 350;
    font-size: var(--content-size);
    font-family: 'Figtree Variable';
    color: var(--content-color-faded);
    max-width: fit-content;

    margin-top: 0;
    margin-bottom: 0.35em;

    /*padding-bottom: 0.25em;*/
    border-bottom: 1px solid currentcolor;
    /* make the border stretch beyond the text just a bit, because I like the effect */
    padding-right: 1.5rem;

    /* SmallCaps is an Astro component so we can't use it here, but we can fake it */
    & .lower {
        font-weight: 500;
    }
}

li {
    position: relative;
    margin-top: 0.45em;
    margin-left: calc(0.75em * (var(--depth) - 2));
    font-size: var(--content-size-sm);
    /* make sure that one item wrapped across multiple lines doesn't just look like multiple items */
    line-height: 1.15;

    &[data-current="true"], &:hover {
        color: var(--content-color);
    }
}

.marker {
    position: absolute;
    left: -0.6rem;
    top: 0.05em;
    bottom: 0.2em;
    width: 0.125rem;
    background-color: var(--accent-color);
}

a {
    color: inherit;
    text-decoration: none;
}

ul {
    margin: 0;
    padding: 0;
    list-style: none;
}
</style>
