// This is definitely not a three.js ripoff.

import { addSpell } from "treehouse/spells.js";
import * as ulid from "treehouse/ulid.js";

/* Branch persistence */

const branchStateKey = "treehouse.openBranches";
let branchState = JSON.parse(sessionStorage.getItem(branchStateKey)) || {};

function saveBranchIsOpen(branchID, state) {
    branchState[branchID] = state;
    sessionStorage.setItem(branchStateKey, JSON.stringify(branchState));
}

function branchIsOpen(branchID) {
    return branchState[branchID];
}

function dateToString(date) {
    return new Date(date - date.getTimezoneOffset() * 60000).toISOString().substring(0, 10);
}

export class Branch {
    static branchesByNamedID = new Map();
    static onAdded = [];

    constructor(element) {
        this.element = element;

        this.isLeaf = element.classList.contains("leaf");

        this.details = element.childNodes[0];
        this.innerUL = this.details.childNodes[1];

        if (this.isLeaf) {
            this.contentContainer = element.childNodes[0];
        } else {
            this.contentContainer = this.details.childNodes[0];
        }
        this.bulletPoint = this.contentContainer.childNodes[0];
        this.branchContent = this.contentContainer.childNodes[1];
        this.buttonBar = this.contentContainer.childNodes[2];

        let doPersist = !element.hasAttribute("th-do-not-persist");
        let isOpen = branchIsOpen(element.id);
        if (doPersist && isOpen !== undefined) {
            this.details.open = isOpen;
        }
        if (!this.isLeaf) {
            this.details.addEventListener("toggle", (_) => {
                saveBranchIsOpen(element.id, this.details.open);
            });
        }

        this.namedID = element.id.replace(/^b-/, "");
        Branch.branchesByNamedID.set(this.namedID, element);

        // Adjust dates to fit the user's time zone.
        let timestamp = null;
        if (element.hasAttribute("th-ts")) {
            timestamp = new Date(parseInt(element.getAttribute("th-ts")));
        } else if (ulid.isCanonicalUlid(this.namedID)) {
            timestamp = ulid.getTimestamp(this.namedID);
        }
        if (timestamp != null) {
            let branchDate = this.buttonBar.getElementsByTagName("th-bd")[0];
            branchDate.textContent = dateToString(timestamp);
        }

        for (let callback of Branch.onAdded) {
            callback(element, this);
        }
    }
}

addSpell("b", Branch);

/* Linked branches */

class LinkedBranch extends Branch {
    static byLink = new Map();

    constructor(element) {
        super(element);

        this.linkedTree = element.getAttribute("th-link");
        LinkedBranch.byLink.set(this.linkedTree, this);

        this.loadingText = document.createElement("p");
        {
            this.loadingText.className = "link-loading";
            let linkedTreeName = document.createElement("code");
            linkedTreeName.innerText = this.linkedTree;
            this.loadingText.append("Loading ", linkedTreeName, "...");
        }
        this.innerUL.appendChild(this.loadingText);

        // This produces a warning during static generation but we still want to handle that
        // correctly, as Branch saves the state in localStorage. Having an expanded-by-default
        // linked block can be useful in development.
        if (this.details.open) {
            this.loadTree("constructor");
        }

        this.details.addEventListener("toggle", (_) => {
            if (this.details.open) {
                this.loadTree("toggle");
            }
        });
    }

    async loadTreePromise(_initiator) {
        try {
            let response = await fetch(`${TREEHOUSE_SITE}/${this.linkedTree}`);
            if (response.status == 404) {
                throw `Hmm, seems like the tree "${this.linkedTree}" does not exist.`;
            }

            let text = await response.text();
            let parser = new DOMParser();
            let linkedDocument = parser.parseFromString(text, "text/html");
            let main = linkedDocument.getElementsByTagName("main")[0];
            let ul = main.getElementsByTagName("ul")[0];
            let styles = main.getElementsByTagName("link");
            let scripts = main.getElementsByTagName("script");

            this.loadingText.remove();
            this.innerUL.innerHTML = ul.innerHTML;

            this.element.append(...styles);
            for (let script of scripts) {
                // No need to await for the import because we don't use the resulting module.
                // Just fire and forger 💀
                // and let them run in parallel.
                let url = URL.createObjectURL(
                    new Blob([script.textContent], { type: "text/javascript" }),
                );
                import(url);
            }
        } catch (error) {
            this.loadingText.innerText = error.toString();
        }
    }

    loadTree(_why) {
        if (!this.loading) {
            this.loading = this.loadTreePromise();
        }
        return this.loading;
    }
}

addSpell("b-linked", LinkedBranch);

/* Fragment navigation */

function expandDetailsRecursively(element) {
    while (element && element.tagName != "MAIN") {
        if (element.tagName == "DETAILS") {
            element.open = true;
        }
        element = element.parentElement;
    }
}

function getCurrentlyHighlightedBranch() {
    if (window.location.hash.length > 0) {
        return window.location.hash.substring(1);
    }
}

async function highlightCurrentBranch() {
    let branchId = getCurrentlyHighlightedBranch();
    if (branchId == null) return;

    let branch = document.getElementById(branchId);
    if (branch != null) {
        branch.scrollIntoView();

        // Expand the linked branch so that the person entering the link doesn't have to click on it.
        expandDetailsRecursively(branch);
        if (branch.children.length > 0 && branch.children[0].tagName == "DETAILS") {
            expandDetailsRecursively(branch.children[0]);
        }

        branch.classList.add("target");
    }
}

addEventListener("DOMContentLoaded", highlightCurrentBranch);
