/**
 * action_fit_text.ts
 *
 * Svelte action that auto-scales text inside a fixed-size container using
 * binary search. Reacts to content changes (MutationObserver) and container
 * resize (ResizeObserver).
 *
 * Usage:
 *   <div use:fit_text={{ min: 14, max: 48 }}>Some text</div>
 *
 * Pass null/undefined to disable (e.g. when a manual font size is active):
 *   <div use:fit_text={manual_size != null ? null : { min: 14, max: 48 }}>
 */

export interface FitTextParams {
    min?: number;
    max?: number;
}

export function fit_text(
    node: HTMLElement,
    params: FitTextParams | null | undefined
) {
    if (!params) return {};

    let { min = 16, max = 80 } = params;

    // overflow:hidden is required so scrollWidth/scrollHeight accurately reflect overflow
    const prev_overflow = node.style.overflow;
    node.style.overflow = 'hidden';

    function fits(): boolean {
        return (
            node.scrollWidth <= node.offsetWidth &&
            node.scrollHeight <= node.offsetHeight
        );
    }

    function fit() {
        if (!params) return;

        // Try max first — if it fits, no search needed
        node.style.fontSize = max + 'px';
        if (fits()) return;

        // Binary search between min and max
        let lo = min;
        let hi = max;
        while (lo < hi - 1) {
            const mid = Math.floor((lo + hi) / 2);
            node.style.fontSize = mid + 'px';
            if (fits()) {
                lo = mid;
            } else {
                hi = mid;
            }
        }
        node.style.fontSize = lo + 'px';
    }

    // Re-fit when text content changes (handles {@html} updates)
    const mutation_observer = new MutationObserver(fit);
    mutation_observer.observe(node, {
        childList: true,
        subtree: true,
        characterData: true
    });

    // Re-fit when the container is resized (e.g. window resize, panel open/close)
    const resize_observer = new ResizeObserver(fit);
    resize_observer.observe(node);

    // Defer initial fit to after the DOM has laid out
    requestAnimationFrame(fit);

    return {
        update(new_params: FitTextParams | null | undefined) {
            if (!new_params) {
                node.style.overflow = prev_overflow;
                return;
            }
            params = new_params;
            min = new_params.min ?? 16;
            max = new_params.max ?? 80;
            node.style.overflow = 'hidden';
            requestAnimationFrame(fit);
        },
        destroy() {
            mutation_observer.disconnect();
            resize_observer.disconnect();
            node.style.overflow = prev_overflow;
        }
    };
}
