{% set rootprefix = "../" * module.modulename.count(".") if module else "" %}
<script>{% include "elasticlunr.min.js" %}</script>
<script>
    function escapeHTML(html) {
        return document.createElement('div').appendChild(document.createTextNode(html)).parentNode.innerHTML;
    }

    const originalContent = document.querySelector("main.pdoc");
    let currentContent = originalContent;

    function setContent(innerHTML) {
        {# Replace the entire page contents. Calling this with an empty argument restores the original page. #}
        let elem;
        if (innerHTML) {
            elem = document.createElement("main");
            elem.classList.add("pdoc");
            elem.innerHTML = innerHTML;
        } else {
            elem = originalContent;
        }
        if (currentContent !== elem) {
            currentContent.replaceWith(elem);
            currentContent = elem;
        }
    }

    function getSearchTerm() {
        return (new URL(window.location)).searchParams.get("search");
    }

    {# the control flow here is: search input -> update location -> onInput #}
    const searchBox = document.querySelector(".pdoc input[type=search]");
    searchBox.addEventListener("input", function () {
        let url = new URL(window.location);
        if (searchBox.value.trim()) {
            url.hash = "";
            url.searchParams.set("search", searchBox.value);
        } else {
            url.searchParams.delete("search");
        }
        history.replaceState("", "", url.toString());
        onInput();
    });
    window.addEventListener("popstate", onInput);


    let searchIndex, searchErr;

    async function initialize() {
        {# Get the search index and compile it if necessary.
           This function will only be called once. #}
        let docs;
        try {
            let resp = await fetch("{{ rootprefix }}search.json");
            docs = await resp.json();
        } catch (e) {
            searchErr = "Cannot fetch search index.";
            throw e;
        }

        {# mirrored in build-search-index.js (part 1) #}
        // Also split on html tags. this is a cheap heuristic, but good enough.
        elasticlunr.tokenizer.setSeperator(/[\s\-.;&]+|<[^>]*>/);

        if (docs._isPrebuiltIndex) {
            console.info("using precompiled search index");
            searchIndex = elasticlunr.Index.load(docs);
        } else {
            console.time("building search index");
            {# mirrored in build-search-index.js (part 2) #}
            searchIndex = elasticlunr(function () {
                this.addField('qualname');
                this.addField('fullname');
                this.addField('doc');
                this.setRef('fullname');
            });
            for (let doc of docs) {
                searchIndex.addDoc(doc);
            }
            console.timeEnd("building search index");
        }
        onInput();

        {# if the user clicks an anchor link in the navigation, we need to restore the original contents. #}
        document.querySelector("nav.pdoc").addEventListener("click", e => {
            if (e.target.hash) {
                searchBox.value = "";
                searchBox.dispatchEvent(new Event("input"));
            }
        });
    }

    function onInput() {
        setContent((() => {
            const search = getSearchTerm();
            if (!search) {
                return null
            }
            if (searchErr) {
                return `<h3>Error: ${searchErr}</h3>`
            }
            if (!searchIndex) {
                return "<h3>Searching...</h3>"
            }

            window.scrollTo({top: 0, left: 0, behavior: 'auto'});

            const results = searchIndex.search(search, {
                fields: {
                    qualname: {boost: 4},
                    fullname: {boost: 2},
                    doc: {boost: 1},
                },
                expand: true
            });

            let html;
            if (results.length === 0) {
                html = `No search results for '${escapeHTML(search)}'.`
            } else {
                html = `<h4>${results.length} search result${results.length > 1 ? "s" : ""} for '${escapeHTML(search)}'.</h4>`;
            }
            for (let result of results.slice(0, 10)) {
                let doc = result.doc;
                let url = `{{ rootprefix }}${doc.modulename.replaceAll(".", "/")}.html`;
                if (doc.qualname) {
                    url += `#${doc.qualname}`;
                }

                let heading;
                switch (result.doc.type) {
                    case "function":
                        heading = `<span class="def">${doc.funcdef}</span> <span class="name">${doc.fullname}</span><span class="signature">(${doc.parameters.join(", ")})</span>`;
                        break;
                    case "class":
                        heading = `<span class="def">class</span> <span class="name">${doc.fullname}</span>`;
                        break;
                    default:
                        heading = `<span class="name">${doc.fullname}</span>`;
                        break;
                }
                html += `
                        <section class="search-result">
                        <a href="${url}" class="attr ${doc.type}">${heading}</a>
                        <div class="docstring">${doc.doc}</div>
                        </section>
                    `;

            }
            return html;
        })());
    }

    if (getSearchTerm()) {
        initialize();
        searchBox.value = getSearchTerm();
        onInput();
    } else {
        searchBox.addEventListener("focus", initialize, {once: true});
    }

    {# keyboard navigation for results #}
    searchBox.addEventListener("keydown", e => {
        if (["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) {
            let focused = currentContent.querySelector(".search-result.focused");
            if (!focused) {
                currentContent.querySelector(".search-result").classList.add("focused");
            } else if (
                e.key === "ArrowDown"
                && focused.nextElementSibling
                && focused.nextElementSibling.classList.contains("search-result")
            ) {
                focused.classList.remove("focused");
                focused.nextElementSibling.classList.add("focused");
                focused.nextElementSibling.scrollIntoView({
                    behavior: "smooth",
                    block: "nearest",
                    inline: "nearest"
                });
            } else if (
                e.key === "ArrowUp"
                && focused.previousElementSibling
                && focused.previousElementSibling.classList.contains("search-result")
            ) {
                focused.classList.remove("focused");
                focused.previousElementSibling.classList.add("focused");
                focused.previousElementSibling.scrollIntoView({
                    behavior: "smooth",
                    block: "nearest",
                    inline: "nearest"
                });
            } else if (
                e.key === "Enter"
            ) {
                focused.querySelector("a").click();
            }
        }
    });
</script>
