import mermaid from {{ mermaid_js_url }};

{% if mermaid_include_elk %}
import elkLayouts from {{ mermaid_elk_js_url }}
{% endif %}

const initStyles = () => {
    const defaultStyle = document.createElement('style');
    defaultStyle.textContent = {{ common_css }};
    document.head.appendChild(defaultStyle);

    const fullscreenStyle = document.createElement('style');
    fullscreenStyle.textContent = {{ fullscreen_css }};
    document.head.appendChild(fullscreenStyle);
}

// Detect if page has dark background
const isDarkTheme = () => {
    // We use a set of heuristics:
    // 1. Check for common dark mode classes or attributes
    // 2. Check computed background color brightness
    if (document.documentElement.classList.contains('dark') ||
        document.documentElement.getAttribute('data-theme') === 'dark' ||
        document.body.classList.contains('dark') ||
        document.body.getAttribute('data-theme') === 'dark') {
        // console.log("Dark theme detected via class/attribute");
        return true;
    }
    if (document.documentElement.classList.contains('light') ||
        document.documentElement.getAttribute('data-theme') === 'light' ||
        document.body.classList.contains('light') ||
        document.body.getAttribute('data-theme') === 'light') {
        // console.log("Light theme detected via class/attribute");
        return false;
    }
    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
        // console.log("Dark theme detected via prefers-color-scheme");
        return true;
    }
    const bgColor = window.getComputedStyle(document.body).backgroundColor;
    const match = bgColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)/);
    if (match) {
        const r = parseInt(match[1]);
        const g = parseInt(match[2]);
        const b = parseInt(match[3]);
        const brightness = (r * 299 + g * 587 + b * 114) / 1000;
        // console.log("Background color brightness:", brightness);
        return brightness < 128;
    }
    // console.log("No dark or light theme detected, defaulting to light theme");
    return false;
};

let darkTheme = isDarkTheme();
let modal = null;
let modalContent = null;
let previousScrollOffset = [window.scrollX, window.scrollY];
let _lazyObserver = null;
let _renderQueue = Promise.resolve();
let closeModal = () => {};

document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && modal?.classList.contains('active')) {
        closeModal();
    }
});

// Apply d3 zoom to each SVG. Idempotent: an SVG already wrapped is skipped,
// so this is safe to call both for the initial batch and for diagrams that
// render lazily once they become visible.
const addZoomToSvgs = (svgs) => {
    svgs.each(function() {
        if (this.getAttribute('data-zoom-applied') === 'true') return;
        var svg = d3.select(this);
        svg.html("<g class='wrapper'>" + svg.html() + "</g>");
        var inner = svg.select("g");
        var zoom = d3.zoom().on("zoom", function(event) {
            inner.attr("transform", event.transform);
        });
        svg.call(zoom);
        this.setAttribute('data-zoom-applied', 'true');
    });
};

const runMermaid = async (rerun) => {
    console.log("Running mermaid diagrams, rerun =", rerun);
    // clear all existing mermaid charts
    let all_mermaids = document.querySelectorAll(".mermaid");

    if (rerun) {
        // Disconnect any previous lazy rendering observer
        if (_lazyObserver) {
            _lazyObserver.disconnect();
            _lazyObserver = null;
        }

        all_mermaids.forEach((el) => {
            el.removeAttribute('data-mermaid-deferred');
            el.removeAttribute('data-mermaid-render-failed');
            if(!el.hasAttribute("data-original-code")) {
                // store original code
                // console.log(`Storing original code for first run: `, el.innerHTML);
                el.setAttribute('data-original-code', el.innerHTML);
            }
            if(el.getAttribute("data-processed") === "true") {
                // remove and restore original
                el.removeAttribute("data-processed");
                // console.log(`Restoring original code for re-run: `, el.getAttribute('data-original-code'));
                el.innerHTML = el.getAttribute('data-original-code');
            } else {
                // store original code
                // console.log(`Storing original code for re-run: `, el.innerHTML);
                el.setAttribute('data-original-code', el.innerHTML);
            }
        });

        // Separate visible and hidden elements. Mermaid's layout engine measures
        // text dimensions via the DOM, which fails for elements hidden by a parent
        // (e.g., Reveal.js non-active slides, sphinx-design unopened tabs),
        // producing broken SVGs. Render visible elements now, defer hidden ones.
        const visible = [];
        const hidden = [];
        all_mermaids.forEach((el) => {
            // offsetParent is null for display:none ancestors.
            // getClientRects().length > 0 catches position:fixed elements
            // (which also have null offsetParent but are still visible).
            if (el.offsetParent !== null || el.getClientRects().length > 0) {
                visible.push(el);
            } else {
                hidden.push(el);
                el.setAttribute('data-mermaid-deferred', 'true');
            }
        });

        if (visible.length > 0) {
            try {
                await mermaid.run({ nodes: visible });
            } catch (e) {
                console.error("Mermaid rendering failed:", e);
            }
        }

        // Lazily render hidden elements when they become visible.
        // Uses a promise chain (_renderQueue) to serialize rendering,
        // since IntersectionObserver may report multiple entries at once
        // and mermaid.run() is not safe to call concurrently.
        if (hidden.length > 0) {
            if (typeof IntersectionObserver === 'undefined') {
                console.warn("IntersectionObserver not available; hidden mermaid diagrams will not render.");
            } else {
                _renderQueue = Promise.resolve();
                _lazyObserver = new IntersectionObserver((entries) => {
                    for (const entry of entries) {
                        if (entry.isIntersecting && entry.target.getAttribute("data-processed") !== "true") {
                            _lazyObserver.unobserve(entry.target);
                            const el = entry.target;
                            _renderQueue = _renderQueue.then(async () => {
                                try {
                                    await mermaid.run({ nodes: [el] });
                                    el.removeAttribute('data-mermaid-deferred');
                                    // Apply zoom to the now-rendered diagram, matching
                                    // the decoration visible diagrams receive.
                                    if ({{ add_zoom }}) {
                                        addZoomToSvgs(d3.selectAll({{ d3_selector }}).select(function() {
                                            return this.querySelector("svg");
                                        }));
                                    }
                                } catch (e) {
                                    console.error("Mermaid deferred rendering failed:", e);
                                    el.removeAttribute('data-mermaid-deferred');
                                    el.setAttribute('data-mermaid-render-failed', 'true');
                                }
                            });
                        }
                    }
                });
                hidden.forEach((el) => _lazyObserver.observe(el));
            }
        }
    }

    all_mermaids = document.querySelectorAll(".mermaid");
    // Exclude deferred (hidden) and render-failed elements from completion checks
    const mermaids_active = document.querySelectorAll(".mermaid:not([data-mermaid-deferred]):not([data-mermaid-render-failed])");
    const mermaids_processed = document.querySelectorAll(".mermaid:not([data-mermaid-deferred]):not([data-mermaid-render-failed])[data-processed='true']");

    if ({{ add_zoom }}) {
        const mermaids_to_add_zoom = {{ d3_node_count }} === -1 ? mermaids_active.length : {{ d3_node_count }};
        if(mermaids_to_add_zoom > 0) {
            var svgs = d3.selectAll({{ d3_selector }}).select(function() {
                return this.querySelector("svg");
            });
            // Wait until every active (visible) diagram has rendered. Deferred
            // diagrams are excluded, so a fixed d3_node_count that counts a
            // hidden diagram no longer loops forever waiting for its SVG.
            if(mermaids_active.length !== mermaids_processed.length) {
                setTimeout(() => runMermaid(false), 200);
                return;
            }
            addZoomToSvgs(svgs);
        }
    } else if(mermaids_active.length !== mermaids_processed.length) {
        // Wait for mermaid to process all diagrams
        setTimeout(() => runMermaid(false), 200);
        return;
    }

    // Stop here if not adding fullscreen capability
    if (!{{ add_fullscreen }}) return;

    if (modal !== null ) {
        // Destroy existing modal
        modal.remove();
        modal = null;
        modalContent = null;
    }

    modal = document.createElement('div');
    modal.className = 'mermaid-fullscreen-modal' + (darkTheme ? ' dark-theme' : '');
    modal.setAttribute('role', 'dialog');
    modal.setAttribute('aria-modal', 'true');
    modal.setAttribute('aria-label', 'Fullscreen diagram viewer');
    modal.innerHTML = `
        <button class="mermaid-fullscreen-close${darkTheme ? ' dark-theme' : ''}" aria-label="Close fullscreen">✕</button>
        <div class="mermaid-container-fullscreen${darkTheme ? ' dark-theme' : ''}"></div>
    `;
    document.body.appendChild(modal);

    modalContent = modal.querySelector('.mermaid-container-fullscreen');
    const closeBtn = modal.querySelector('.mermaid-fullscreen-close');

    closeModal = () => {
        modal.classList.remove('active');
        modalContent.innerHTML = '';
        document.body.style.overflow = ''
        window.scrollTo({left: previousScrollOffset[0], top: previousScrollOffset[1], behavior: 'instant'});
    };

    closeBtn.addEventListener('click', closeModal);
    modal.addEventListener('click', (e) => {
        if (e.target === modal) closeModal();
    });
    document.querySelectorAll('.mermaid').forEach((mermaidDiv) => {
        if (mermaidDiv.parentNode.classList.contains('mermaid-container') ||
            mermaidDiv.closest('.mermaid-fullscreen-modal')) {
            // Already processed, adjust button class if needed
            const existingBtn = mermaidDiv.parentNode.querySelector('.mermaid-fullscreen-btn');
            if (existingBtn) {
                existingBtn.className = 'mermaid-fullscreen-btn' + (darkTheme ? ' dark-theme' : '');
            }
            return;
        }

        const container = document.createElement('div');
        container.className = 'mermaid-container';
        mermaidDiv.parentNode.insertBefore(container, mermaidDiv);
        container.appendChild(mermaidDiv);

        const fullscreenBtn = document.createElement('button');
        fullscreenBtn.className = 'mermaid-fullscreen-btn' + (darkTheme ? ' dark-theme' : '');
        fullscreenBtn.setAttribute('aria-label', 'View diagram in fullscreen');
        fullscreenBtn.textContent = {{ button_text }};
        fullscreenBtn.style.opacity = {{ button_opacity }};

        // Calculate dynamic position based on diagram's margin and padding
        const diagramStyle = window.getComputedStyle(mermaidDiv);
        const marginTop = parseFloat(diagramStyle.marginTop) || 0;
        const marginRight = parseFloat(diagramStyle.marginRight) || 0;
        const paddingTop = parseFloat(diagramStyle.paddingTop) || 0;
        const paddingRight = parseFloat(diagramStyle.paddingRight) || 0;
        fullscreenBtn.style.top = `${marginTop + paddingTop + 4}px`;
        fullscreenBtn.style.right = `${marginRight + paddingRight + 4}px`;

        fullscreenBtn.addEventListener('click', () => {
            previousScrollOffset = [window.scrollX, window.scrollY];
            const clone = mermaidDiv.cloneNode(true);
            modalContent.innerHTML = '';
            modalContent.appendChild(clone);

            const svg = clone.querySelector('svg');
            if (svg) {
                svg.removeAttribute('width');
                svg.removeAttribute('height');
                svg.style.width = '100%';
                svg.style.height = 'auto';
                svg.style.maxWidth = '100%';
                svg.style.display = 'block';

                if ({{ add_zoom }}) {
                    setTimeout(() => {
                        const g = svg.querySelector('g');
                        if (g) {
                            var svgD3 = d3.select(svg);
                            svgD3.html("<g class='wrapper'>" + svgD3.html() + "</g>");
                            var inner = svgD3.select("g");
                            var zoom = d3.zoom().on("zoom", function(event) {
                                inner.attr("transform", event.transform);
                            });
                            svgD3.call(zoom);
                        }
                    }, 100);
                }
            }

            modal.classList.add('active');
            document.body.style.overflow = 'hidden';
        });
        container.appendChild(fullscreenBtn);
    });
};

{% if mermaid_include_zenuml %}
// Resolves once the lazily-loaded zenuml plugin has registered (or immediately
// when the page has no zenuml diagram). `load` awaits this before rendering.
let zenumlReady = Promise.resolve();
const pageHasZenuml = () => [...document.querySelectorAll(".mermaid")].some((el) => {
    const code = el.getAttribute("data-original-code") || el.textContent || "";
    const diagram = code
        .replace(/^\s*---\s*\n[^]*?\n---\s*/, "")
        .replace(/^\s*%%\{[^]*?\}%%\s*/, "");
    return /^\s*zenuml\b/i.test(diagram);
});
{% endif %}

const load = async () => {
    initStyles();

{% if mermaid_include_zenuml %}
    // Wait for the zenuml plugin (loaded lazily below) to finish registering
    // before rendering, so zenuml diagrams are recognised.
    await zenumlReady;
{% endif %}
    await runMermaid(true);

    const reRunIfThemeChanges = async () => {
        const newDarkTheme = isDarkTheme();
        if (newDarkTheme !== darkTheme) {
            darkTheme = newDarkTheme;
            console.log("Theme change detected, re-running mermaid with", darkTheme ? {{ mermaid_dark_theme }} : {{ mermaid_light_theme }}, "theme");
            await mermaid.initialize(
                {...{{ mermaid_init_config }},
                ...{ darkMode: darkTheme, theme: darkTheme ? {{ mermaid_dark_theme }} : {{ mermaid_light_theme }} },
                }
            );
            await runMermaid(true);
        }
    };

    // Update theme classes when theme changes
    const themeObserver = new MutationObserver(reRunIfThemeChanges);
    themeObserver.observe(document.documentElement, {
        attributes: true,
        attributeFilter: ['class', 'style', 'data-theme']
    });
    themeObserver.observe(document.body, {
        attributes: true,
        attributeFilter: ['class', 'style', 'data-theme']
    });
};

{% if mermaid_include_elk %}
mermaid.registerLayoutLoaders(elkLayouts);
{% endif %}

{% if mermaid_include_zenuml %}
// Only load and register the zenuml plugin when the page actually contains a
// zenuml diagram. This avoids fetching the (large) zenuml bundle on every page,
// and avoids its side effects where they are not needed. The import is kept off
// the module's top level (no top-level await) so the "load" listener below is
// still registered synchronously; `load` awaits `zenumlReady` before rendering.
if (pageHasZenuml()) {
    // The zenuml bundle injects a stylesheet that declares generic design tokens
    // (--background, --border, --white, ...) on :root, clobbering identically-named
    // tokens in the host theme (sphinxawesome, shibuya, ...) and causing a flash of
    // restyled page content. Confine those declarations to the zenuml diagram
    // subtree so they no longer leak onto the host page.
    const descopeZenumlStyle = (node) => {
        if (!node || node.tagName !== "STYLE" || node.hasAttribute("data-zenuml-descoped")) return;
        const css = node.textContent || "";
        if (!css.includes("--occurance-border")) return; // zenuml stylesheet signature
        node.setAttribute("data-zenuml-descoped", "true");
        node.textContent = css.replace(/:root\b/g, ".zenuml");
    };
    new MutationObserver((records) => {
        for (const rec of records) {
            for (const node of rec.addedNodes) descopeZenumlStyle(node);
        }
    }).observe(document.head, { childList: true });

    zenumlReady = import({{ mermaid_zenuml_js_url }}).then(({ default: zenumlLayouts }) => {
        mermaid.registerExternalDiagrams([zenumlLayouts]);
    });
}
{% endif %}

console.log("Initializing mermaid with", darkTheme ? {{ mermaid_dark_theme }} : {{ mermaid_light_theme }}, "theme");
mermaid.initialize(
    {...{{ mermaid_init_config }},
    ...{ darkMode: darkTheme, theme: darkTheme ? {{ mermaid_dark_theme }} : {{ mermaid_light_theme }} },
    }
);

window.addEventListener("load", load);
window.runMermaid = runMermaid;
