const defaultTimerApi = {
    setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
    clearTimeout: (handle) => clearTimeout(handle),
};
const defaultScheduler = {
    queueMicrotask: (handler) => queueMicrotask(handler),
};
function getRequestRenderFromHost(host) {
    if (typeof host !== "object" || host === null)
        return undefined;
    const candidate = host.requestRender;
    if (typeof candidate !== "function")
        return undefined;
    return () => candidate.call(host);
}
function linesEqual(a, b) {
    if (a.length !== b.length)
        return false;
    for (let i = 0; i < a.length; i++) {
        if (a[i] !== b[i])
            return false;
    }
    return true;
}
export function decideReactiveWidgetAction(prev, nextLines) {
    const nextVisible = nextLines.length > 0;
    if (!prev.mounted)
        return nextVisible ? "mount" : "none";
    if (!nextVisible)
        return "unmount";
    return linesEqual(prev.lines, nextLines) ? "none" : "update";
}
export function installReactiveWidget(options) {
    const timers = options.timers ?? defaultTimerApi;
    const scheduler = options.scheduler ?? defaultScheduler;
    const now = options.now ?? Date.now;
    const coalesceRenderRequests = options.coalesceRenderRequests ?? true;
    const requestRenderOnMount = options.requestRenderOnMount ?? true;
    const requestRenderOnUnmount = options.requestRenderOnUnmount ?? true;
    const requestRenderOnStateNoop = options.requestRenderOnStateNoop ?? true;
    let disposed = false;
    let mounted = false;
    let renderQueued = false;
    let mountedRequestRender;
    let refreshTimer;
    let currentSnap = options.getSnapshot();
    let currentNow = now();
    let lastLines = [];
    const clearRefreshTimer = () => {
        if (refreshTimer === undefined)
            return;
        timers.clearTimeout(refreshTimer);
        refreshTimer = undefined;
    };
    const handleError = (error) => {
        if (options.isStaleError?.(error))
            return;
        throw error;
    };
    const invokeRequestRender = () => {
        if (options.ui.requestRender) {
            options.ui.requestRender();
            return;
        }
        mountedRequestRender?.();
    };
    const requestRender = () => {
        if (!coalesceRenderRequests) {
            try {
                invokeRequestRender();
            }
            catch (error) {
                handleError(error);
            }
            return;
        }
        if (renderQueued)
            return;
        renderQueued = true;
        scheduler.queueMicrotask(() => {
            renderQueued = false;
            if (disposed)
                return;
            try {
                invokeRequestRender();
            }
            catch (error) {
                handleError(error);
            }
        });
    };
    const requestRenderForDispose = () => {
        renderQueued = false;
        invokeRequestRender();
    };
    const scheduleRefresh = () => {
        const delayMs = options.getNextRefreshDelayMs?.(currentSnap, currentNow);
        if (delayMs === undefined)
            return;
        refreshTimer = timers.setTimeout(() => {
            refreshTimer = undefined;
            controller.refresh("clock");
        }, delayMs);
        refreshTimer.unref?.();
    };
    const widgetFactory = (tui, theme) => {
        const fallbackRequestRender = getRequestRenderFromHost(tui);
        if (fallbackRequestRender)
            mountedRequestRender = fallbackRequestRender;
        return {
            render(width) {
                return [...options.render(currentSnap, { theme, width, now: currentNow })];
            },
            invalidate() {
                // The component is intentionally stateless; callers should rebuild from the
                // latest snapshot/time on every render. This hook exists so pi-tui can still
                // invalidate the long-lived component on theme changes.
            },
        };
    };
    const controller = {
        refresh(reason = "manual") {
            if (disposed)
                return;
            clearRefreshTimer();
            try {
                currentSnap = options.getSnapshot();
                currentNow = now();
                const nextLines = options.getPreviewLines(currentSnap, currentNow);
                const action = decideReactiveWidgetAction({ mounted, lines: lastLines }, nextLines);
                switch (action) {
                    case "mount":
                        options.ui.setWidget(options.key, widgetFactory, options.placement ? { placement: options.placement } : undefined);
                        mounted = true;
                        if (requestRenderOnMount)
                            requestRender();
                        break;
                    case "unmount":
                        options.ui.setWidget(options.key, undefined);
                        mounted = false;
                        if (requestRenderOnUnmount)
                            requestRender();
                        break;
                    case "update":
                        requestRender();
                        break;
                    case "none":
                        if (mounted && reason === "state" && requestRenderOnStateNoop)
                            requestRender();
                        break;
                }
                lastLines = nextLines;
                scheduleRefresh();
            }
            catch (error) {
                handleError(error);
            }
        },
        dispose() {
            if (disposed)
                return;
            disposed = true;
            clearRefreshTimer();
            unsubscribe?.();
            try {
                options.ui.setWidget(options.key, undefined);
                mounted = false;
                requestRenderForDispose();
            }
            catch (error) {
                handleError(error);
            }
        },
        isMounted() {
            return mounted;
        },
    };
    const unsubscribe = options.subscribe?.(() => controller.refresh("state"));
    controller.refresh("initial");
    return controller;
}
//# sourceMappingURL=reactive-widget.js.map