/* This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */

class Heading {
    id: string;
    html: string;
    parent: Heading | undefined;

    constructor(id: string, html: string, parent: Heading | undefined) {
        this.id = id;
        this.html = html;
        this.parent = parent;
    }
}

const DEBUG = false;

function removeIds(elem: Element): void {
    for (const c of elem.children) {
        c.removeAttribute("id");
        removeIds(c);
    }
}

let glossaryDoc: Document | undefined;
async function fetchGlossDoc(): Promise<Document> {
    if (glossaryDoc) {
        return glossaryDoc;
    }

    const res = await fetch("/glossary/", {
        cache: "default",
        method: "GET",
        mode: "same-origin",
    });
    if (!res.ok) {
        throw new Error(
            `Got a non-OK status from /glossary/ : ${res.status} ${res.statusText}`,
        );
    }

    const html = await res.text();
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, "text/html");
    glossaryDoc = doc;

    return doc;
}
const glossSidenotes: Map<string, HTMLDivElement> = new Map();
async function getGlossSidenote(id: string): Promise<HTMLDivElement> {
    const cached = glossSidenotes.get(id);
    if (cached) {
        return cached;
    }

    const doc = await fetchGlossDoc();

    const newSidenote = document.createElement("div");
    newSidenote.classList.add("sidenote");
    newSidenote.setAttribute("role", "note");

    const dfnSpan = document.createElement("span");
    dfnSpan.classList.add("side-gloss-span");
    const dt = doc.getElementById(id);
    if (!dt) {
        throw new Error(`No element with id ${id} in glossary document`);
    }
    const dtChild = dt.children[0];
    const dfn = document.createElement("dfn");
    dfn.innerHTML = dtChild ? dtChild.innerHTML : dt.innerHTML;
    for (let i = 0; i < dt.style.length; ++i) {
        const styleProp = dt.style.item(i);
        dfn.style.setProperty(styleProp, dt.style.getPropertyValue(styleProp));
    }
    for (const c of dt.classList) {
        dfn.classList.add(c);
    }
    dfnSpan.append(dfn, ":\u{a0}");
    newSidenote.appendChild(dfnSpan);

    const dd = dt.nextElementSibling;
    if (!dd) {
        throw new Error(`Glossary document #${id} has no nextElementSibling`);
    }
    for (const c of dd.childNodes) {
        const cClone = c.cloneNode(true);
        if (cClone instanceof Element) {
            removeIds(cClone);
        }
        newSidenote.appendChild(cClone);
    }

    glossSidenotes.set(id, newSidenote);

    return newSidenote;
}

export function activateSidenotes(): [() => void, () => void] {
    const main = document.querySelector("main");
    if (!main) {
        console.warn("No `<main>` element");

        return [() => {}, () => {}];
    }

    const sidenotes: Map<string, HTMLDivElement> = new Map();
    let sidenotesActive = true;
    let currentSidenote: HTMLElement | undefined = undefined;
    let currentSidenoteI: number | undefined = undefined;
    let currentMarkSpan: HTMLElement | undefined = undefined;
    main.querySelectorAll("sup.enmark > a, .cite > a").forEach((mark, i) => {
        if (!(mark instanceof HTMLAnchorElement)) {
            return;
        }
        const id = mark.getAttribute("href")?.slice(1);
        if (!id) {
            console.warn("Mark anchor has no href:", mark);

            return;
        }
        const isCite = mark.parentElement?.classList.contains("cite");
        const markReferent = isCite
            ? document.getElementById(id)?.querySelector("td")
            : document.getElementById(id);
        if (!markReferent) {
            console.warn("Mark anchor has bad href:", mark);

            return;
        }

        const markSpan = (() => {
            const parent = mark.parentElement?.parentElement;
            if (
                !isCite &&
                parent &&
                parent.classList.contains("enmark-span")
            ) {
                return parent;
            } else {
                return mark;
            }
        })();

        let previouslyShown = false;
        const showSidenote = () => {
            if (!sidenotesActive || currentSidenoteI === i) {
                return;
            }
            if (currentSidenote) {
                currentSidenote.remove();
            }
            if (currentMarkSpan) {
                currentMarkSpan.classList.remove("enmark-span-highlight");
            }

            const sidenote =
                sidenotes.get(id) ??
                (() => {
                    const newSidenote = document.createElement("div");
                    newSidenote.classList.add("sidenote");
                    newSidenote.setAttribute("role", "note");
                    for (const refChild of markReferent.childNodes) {
                        if (
                            refChild instanceof Element &&
                            refChild.classList.contains("en-arrow")
                        ) {
                            continue;
                        }
                        const liChildClone = refChild.cloneNode(true);
                        if (liChildClone instanceof Element) {
                            removeIds(liChildClone);
                        }
                        newSidenote.appendChild(liChildClone);
                    }
                    sidenotes.set(id, newSidenote);

                    return newSidenote;
                })();

            currentSidenote = sidenote;
            currentSidenoteI = i;
            currentMarkSpan = markSpan;
            main.appendChild(sidenote);

            const markRect = mark.getBoundingClientRect();

            const top = Math.round(
                markRect.top -
                    document.documentElement.clientTop +
                    window.scrollY,
            );
            sidenote.style.top = `${top}px`;

            if (previouslyShown) {
                return;
            }
            previouslyShown = true;

            let [focused, hovered] = [false, false];
            const markSpanHighlight = () =>
                currentMarkSpan?.classList.add("enmark-span-highlight");
            const markSpanUnhighlight = () =>
                currentMarkSpan?.classList.remove("enmark-span-highlight");
            sidenote.addEventListener("focusin", () => {
                focused = true;
                markSpanHighlight();
            });
            sidenote.addEventListener("focusout", () => {
                focused = false;
                if (!hovered) {
                    markSpanUnhighlight();
                }
            });
            sidenote.addEventListener("pointerenter", () => {
                hovered = true;
                markSpanHighlight();
            });
            sidenote.addEventListener("pointerleave", () => {
                hovered = false;
                if (!focused) {
                    markSpanUnhighlight();
                }
            });
        };
        mark.addEventListener("focusin", showSidenote);
        mark.addEventListener("pointerenter", showSidenote);
    });

    main.querySelectorAll(".gloss-ref").forEach((a, i) => {
        i = -2 * (i + 1) - 1;

        if (!(a instanceof HTMLElement)) {
            return;
        }

        const id = a.getAttribute("href")?.split("#")[1];
        if (!id) {
            console.warn("Glossary ref has no `href`:", a);

            return;
        }

        if (DEBUG) {
            getGlossSidenote(id).catch(err => console.error(err));
        }

        let previouslyShown = false;
        const showSidenote = () => {
            if (!sidenotesActive) {
                return;
            }

            const sidenoteIAtInteraction = currentSidenoteI;
            getGlossSidenote(id)
                .then(sidenote => {
                    if (
                        currentSidenoteI === i ||
                        currentSidenoteI !== sidenoteIAtInteraction
                    ) {
                        return;
                    }
                    if (currentSidenote) {
                        currentSidenote.remove();
                    }
                    if (currentMarkSpan) {
                        currentMarkSpan.classList.remove(
                            "enmark-span-highlight",
                        );
                    }
                    currentSidenote = sidenote;
                    currentSidenoteI = i;
                    currentMarkSpan = a;
                    main.appendChild(sidenote);

                    const aRect = a.getBoundingClientRect();

                    const top = Math.round(
                        aRect.top -
                            document.documentElement.clientTop +
                            window.scrollY,
                    );
                    sidenote.style.top = `${top}px`;

                    if (previouslyShown) {
                        return;
                    }
                    previouslyShown = true;

                    let [focused, hovered] = [false, false];
                    const markSpanHighlight = () =>
                        currentMarkSpan?.classList.add(
                            "enmark-span-highlight",
                        );
                    const markSpanUnhighlight = () =>
                        currentMarkSpan?.classList.remove(
                            "enmark-span-highlight",
                        );
                    sidenote.addEventListener("focusin", () => {
                        focused = true;
                        markSpanHighlight();
                    });
                    sidenote.addEventListener("focusout", () => {
                        focused = false;
                        if (!hovered) {
                            markSpanUnhighlight();
                        }
                    });
                    sidenote.addEventListener("pointerenter", () => {
                        hovered = true;
                        markSpanHighlight();
                    });
                    sidenote.addEventListener("pointerleave", () => {
                        hovered = false;
                        if (!focused) {
                            markSpanUnhighlight();
                        }
                    });
                })
                .catch(err => console.error(err));
        };
        a.addEventListener("focusin", showSidenote);
        a.addEventListener("pointerenter", showSidenote);
    });

    const headingStack: Heading[] = [];
    main.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach(
        (headingElem, i) => {
            i = -2 * (i + 1);

            const id = headingElem.getAttribute("id");
            if (!id) {
                console.error("Heading has no id:", headingElem);

                return;
            }
            const innerHtml = headingElem.innerHTML;
            const levelStr = headingElem.tagName[1];
            if (!levelStr) {
                console.error("Unexpected tagName for", headingElem);

                return;
            }
            const level = Number.parseInt(levelStr, 10);

            if (level > headingStack.length + 1) {
                console.warn("Unexpected heading level:", headingElem);
            } else {
                headingStack.length = level - 1;
            }
            const heading = new Heading(
                id,
                innerHtml,
                headingStack[headingStack.length - 1],
            );
            headingStack.push(heading);

            const labelId = headingElem.getAttribute("aria-labelledby");
            if (!labelId) {
                console.error("Heading has no aria-labelledby:", headingElem);

                return;
            }
            const label = document.getElementById(labelId);
            if (!label) {
                console.error("Heading has no aria-labelledby:", headingElem);

                return;
            }

            const sidenote = document.createElement("nav");
            sidenote.classList.add("sidenote");
            sidenote.setAttribute("role", "note");
            const ol = document.createElement("ol");
            ol.classList.add("breadcrumbs");
            let h: Heading | undefined = heading;
            let first = true;
            while (h) {
                const li = document.createElement("li");
                const a = document.createElement("a");
                a.setAttribute("href", `#${h.id}`);
                a.innerHTML = h.html;
                if (first) {
                    a.setAttribute("aria-current", "page");
                }
                li.appendChild(a);
                ol.prepend(li);
                h = h.parent;
                first = false;
            }
            sidenote.appendChild(ol);

            const showSidenote = () => {
                if (!sidenotesActive || currentSidenoteI === i) {
                    return;
                }
                if (currentSidenote) {
                    currentSidenote.remove();
                }
                if (currentMarkSpan) {
                    currentMarkSpan.classList.remove("enmark-span-highlight");
                }
                currentSidenote = sidenote;
                currentSidenoteI = i;
                currentMarkSpan = undefined;
                main.appendChild(sidenote);

                const labelRect = label.getBoundingClientRect();

                const top = Math.round(
                    labelRect.top -
                        document.documentElement.clientTop +
                        window.scrollY,
                );
                sidenote.style.top = `${top}px`;
            };

            label.addEventListener("focusin", showSidenote);
            label.addEventListener("pointerenter", showSidenote);
        },
    );

    const disableSidenotes = () => {
        sidenotesActive = false;
        if (currentSidenote) {
            currentSidenote.remove();
        }
        if (currentMarkSpan) {
            currentMarkSpan.classList.remove("enmark-span-highlight");
        }
        currentSidenote = undefined;
        currentSidenoteI = undefined;
        currentMarkSpan = undefined;
    };
    const enableSidenotes = () => (sidenotesActive = true);

    return [disableSidenotes, enableSidenotes];
}
