console.log("utils.js loaded");

Shiny.initializedPromise.then(() => {
    console.log("Shiny initialized - utils.js");

    // Layout variables: set CSS custom properties for header and footer heights,
    // so that the left menus (portrauyal options, map elements, etc.) can use them
    // for correct sizing and spacing.
    // Updated on window resize to handle responsive layout changes.
    function updateLayoutVars() {
        const header = document.querySelector('header.mapsf-gui-header');
        const footer = document.querySelector('footer.mapsf-gui-footer');
        const hh = header ? header.offsetHeight : 0;
        const fh = footer ? footer.offsetHeight : 0;
        document.documentElement.style.setProperty('--header-height', hh + 'px');
        document.documentElement.style.setProperty('--footer-height', fh + 'px');

        // Also update browser dimensions for Shiny server-side use
        Shiny.setInputValue('browser_width', window.innerWidth);
        Shiny.setInputValue('browser_height', window.innerHeight);
    }

    updateLayoutVars();
    window.addEventListener('resize', updateLayoutVars);

    // Map scale display (tells the user if the map is displayed at a smaller size
    // than the selected one)
    const updateMapScale = () => {
        const img = document.querySelector('#map img');
        const txt = document.getElementById('map-scale');
        if (!img || !txt || !img.naturalWidth) return;
        const scale = Math.min(
            img.clientWidth  / img.naturalWidth,
            img.clientHeight / img.naturalHeight
        );
        const pct = Math.round(scale * 100);
        txt.textContent = (pct < 100)
            ? `Affiché à ~${pct}% de la taille sélectionnée`
            : '';
    };

    const wireMapImg = () => {
        const img = document.querySelector('#map img');
        if (!img) return;
        img.removeEventListener('load', updateMapScale);
        img.addEventListener('load', updateMapScale, { once: false });
        if (img.complete) updateMapScale();
    };

    const mapNode = document.getElementById('map');
    if (mapNode) {
        new MutationObserver(wireMapImg).observe(mapNode, { childList: true, subtree: true });
    } else {
        // Hopefully never happens... ?
        console.warn("Map element not found for image load observer");
    }

    window.addEventListener('resize', () => requestAnimationFrame(updateMapScale));

    // Cancel drag behavior on interactive elements inside
    // the draggable containers (e.g. the <code> element inside code panel)
    $('.draggable-container').draggable('option', 'cancel', 'pre, code, input, textarea, button, select, option');

    // Constrain the draggable containers within the window to prevent them from being lost off-screen
    // TODO: this doesn't totally work as expected: the panels can still be dragged
    // off-screen on the left and bottom edges (but OK on right and top edges).
    $('.draggable-container').draggable('option', 'containment', 'window');

    // Syntax highlighting
    // The observer is disconnected before highlighting to avoid an infinite loop:
    // hljs.highlightElement() mutates the DOM (adds <span> tags), which would
    // otherwise re-trigger the observer indefinitely
    let codeObserver = null;

    const highlightCode = () => {
        const codeEl = document.querySelector('#code code');
        if (!codeEl) return;

        if (codeObserver) codeObserver.disconnect();

        delete codeEl.dataset.highlighted;
        hljs.highlightElement(codeEl);

        if (codeObserver) {
            const container = document.getElementById('code');
            if (container) codeObserver.observe(container, { childList: true, subtree: true });
        }
    };

    const observeCodeContainer = (el) => {
        codeObserver = new MutationObserver(() => requestAnimationFrame(highlightCode));
        codeObserver.observe(el, { childList: true, subtree: true });
        // Highlight any content already present when the observer first attaches
        requestAnimationFrame(highlightCode);
    };

    const codeContainer = document.getElementById('code');
    if (codeContainer) {
        observeCodeContainer(codeContainer);
    } else {
        const bodyObserver = new MutationObserver(() => {
            const el = document.getElementById('code');
            if (!el) return;
            bodyObserver.disconnect();
            observeCodeContainer(el);
        });
        bodyObserver.observe(document.body, { childList: true, subtree: true });
    }

    // Code panel: only the toggle button opens/closes it, nothing else
    const codeBtn = document.getElementById('display_code_btn');
    const codePanel = document.getElementById('code-panel');

    if (codeBtn && codePanel) {
        codeBtn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            const isOpen = !codePanel.classList.contains('d-none');
            codePanel.classList.toggle('d-none', isOpen);
            codeBtn.setAttribute('aria-expanded', String(!isOpen));
            codeBtn.classList.toggle('active', !isOpen);

            // Re-highlight after the panel is visible: hljs needs the element
            // to be painted (not display:none) to apply correct tokenization.
            // A short setTimeout ensures the browser has rendered the panel first.
            if (!isOpen) setTimeout(highlightCode, 0);
        });
    } else {
        // Hopefully never happens... ?
        console.warn("Code panel or button not found");
    }

    // Copy to clipboard
    if (codePanel) {
        codePanel.addEventListener('click', (e) => {
            const btn = e.target.closest('#copy_code');
            if (!btn) return;
            e.stopPropagation();

            const codeEl = document.querySelector('#code code');
            const text = codeEl ? codeEl.textContent : '';

            navigator.clipboard.writeText(text).then(() => {
                const originalHTML = btn.innerHTML;
                btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 256 256" style="vertical-align:middle"><path fill="currentColor" d="m229.66 77.66-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69 218.34 66.34a8 8 0 0 1 11.32 11.32Z"/></svg>`;
                btn.disabled = true;
                setTimeout(() => {
                    btn.innerHTML = originalHTML;
                    btn.disabled = false;
                }, 1000);
            }).catch((err) => {
                console.error("Clipboard write failed:", err);
            });
        });
    }

    // Logic for showing a custom spinner when Shiny is busy
    const spinner = document.createElement('div');
    spinner.id = 'mapsf-busy-spinner';
    spinner.innerHTML = '<span></span><span></span><span></span>';
    // Start with the spinner visible which should be similar to onstart = TRUE
    // shinybusy::add_busy_spinner() behavior, to cover the initial loading time
    // before Shiny is fully initialized
    spinner.classList.add('visible');
    document.body.appendChild(spinner);

    let busyTimeout;
    $(document).on('shiny:busy', function() {
        // Equivalent to timeout = 200 of shinybusy::add_busy_spinner(),
        // to avoid showing the spinner for very short operations...
        busyTimeout = setTimeout(() => spinner.classList.add('visible'), 200);
    });
    $(document).on('shiny:idle', function() {
        clearTimeout(busyTimeout);
        spinner.classList.remove('visible');
    });
});
