/**
 * Minimal TUI implementation with differential rendering
 */
import * as os from "node:os";
import * as path from "node:path";
import { performance } from "node:perf_hooks";
import { isKeyRelease, matchesKey } from "./keys.js";
import { isOsc11BackgroundColorResponse, parseOsc11BackgroundColor, parseTerminalColorSchemeReport, } from "./terminal-colors.js";
import { getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
/** Type guard to check if a component implements Focusable */
export function isFocusable(component) {
    return component !== null && "focused" in component;
}
/**
 * Cursor position marker - APC (Application Program Command) sequence.
 * This is a zero-width escape sequence that terminals ignore.
 * Components emit this at the cursor position when focused.
 * TUI finds and strips this marker, then positions the hardware cursor there.
 */
export const CURSOR_MARKER = "\x1b_pi:c\x07";
export { visibleWidth };
/** Parse a SizeValue into absolute value given a reference size */
function parseSizeValue(value, referenceSize) {
    if (value === undefined)
        return undefined;
    if (typeof value === "number")
        return value;
    // Parse percentage string like "50%"
    const match = value.match(/^(\d+(?:\.\d+)?)%$/);
    if (match) {
        return Math.floor((referenceSize * parseFloat(match[1])) / 100);
    }
    return undefined;
}
/**
 * Container - a component that contains other components
 */
export class Container {
    children = [];
    addChild(component) {
        this.children.push(component);
    }
    removeChild(component) {
        const index = this.children.indexOf(component);
        if (index !== -1) {
            this.children.splice(index, 1);
        }
    }
    clear() {
        this.children = [];
    }
    invalidate() {
        for (const child of this.children) {
            child.invalidate?.();
        }
    }
    render(width) {
        const lines = [];
        for (const child of this.children) {
            const childLines = child.render(width);
            for (const line of childLines) {
                lines.push(line);
            }
        }
        return lines;
    }
}
/**
 * TUI - Main class for managing terminal UI with differential rendering
 */
const SEGMENT_RESET = "\x1b[0m\x1b]8;;\x07";
/** Composite overlay content into a terminal line at a fixed column. */
export function compositeTuiLine(baseLine, overlayLine, startCol, overlayWidth, totalWidth) {
    if (isImageLine(baseLine))
        return baseLine;
    const afterStart = startCol + overlayWidth;
    const base = extractSegments(baseLine, startCol, afterStart, totalWidth - afterStart, true);
    const overlay = sliceWithWidth(overlayLine, 0, overlayWidth, true);
    const beforePad = Math.max(0, startCol - base.beforeWidth);
    const overlayPad = Math.max(0, overlayWidth - overlay.width);
    const actualBeforeWidth = Math.max(startCol, base.beforeWidth);
    const actualOverlayWidth = Math.max(overlayWidth, overlay.width);
    const afterTarget = Math.max(0, totalWidth - actualBeforeWidth - actualOverlayWidth);
    const afterPad = Math.max(0, afterTarget - base.afterWidth);
    const result = base.before +
        " ".repeat(beforePad) +
        SEGMENT_RESET +
        overlay.text +
        " ".repeat(overlayPad) +
        SEGMENT_RESET +
        base.after +
        " ".repeat(afterPad);
    return visibleWidth(result) <= totalWidth ? result : sliceByColumn(result, 0, totalWidth, true);
}
export const VIEWPORT_TUI = Symbol.for("@earendil-works/pi-tui/viewport");
export function isViewportTUI(tui) {
    return tui[VIEWPORT_TUI] === true;
}
export class TuiBase extends Container {
    terminal;
    focusedComponent = null;
    inputListeners = new Set();
    /** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
    onDebug;
    renderRequested = false;
    immediateRenderScheduled = false;
    renderTimer;
    lastRenderAt = 0;
    static MIN_RENDER_INTERVAL_MS = 16;
    showHardwareCursor = process.env.PI_HARDWARE_CURSOR === "1";
    clearOnShrink = process.env.PI_CLEAR_ON_SHRINK === "1";
    fullRedrawCount = 0;
    stopped = false;
    pendingOsc11BackgroundReplies = 0;
    pendingOsc11BackgroundQueries = [];
    terminalColorSchemeListeners = new Set();
    terminalColorSchemeNotificationsEnabled = false;
    logDirectory;
    // Overlay stack for modal components rendered on top of base content
    focusOrderCounter = 0;
    overlayStack = [];
    get hasOverlayEntries() {
        return this.overlayStack.length > 0;
    }
    overlayFocusRestore = { status: "inactive" };
    constructor(terminal, showHardwareCursor, logDirectory) {
        super();
        this.terminal = terminal;
        this.logDirectory = logDirectory ?? process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
        if (showHardwareCursor !== undefined) {
            this.showHardwareCursor = showHardwareCursor;
        }
    }
    resetRenderState() { }
    beforeTerminalStart() { }
    afterTerminalStart() { }
    beforeTerminalStop(_options) { }
    afterTerminalStop(_options) { }
    get fullRedraws() {
        return this.fullRedrawCount;
    }
    getShowHardwareCursor() {
        return this.showHardwareCursor;
    }
    setShowHardwareCursor(enabled) {
        if (this.showHardwareCursor === enabled)
            return;
        this.showHardwareCursor = enabled;
        if (!enabled) {
            this.terminal.hideCursor();
        }
        this.requestRender();
    }
    getClearOnShrink() {
        return this.clearOnShrink;
    }
    /**
     * Set whether to trigger full re-render when content shrinks.
     * When true (default), empty rows are cleared when content shrinks.
     * When false, empty rows remain (reduces redraws on slower terminals).
     */
    setClearOnShrink(enabled) {
        this.clearOnShrink = enabled;
    }
    getFocusedComponent() {
        return this.focusedComponent;
    }
    setFocus(component) {
        this.setFocusInternal({ component, overlayFocusRestore: "clear" });
    }
    setFocusInternal({ component, overlayFocusRestore, }) {
        const previousFocus = this.focusedComponent;
        let nextFocus = component;
        const previousFocusedOverlay = previousFocus
            ? this.overlayStack.find((entry) => entry.component === previousFocus && this.isOverlayVisible(entry))
            : undefined;
        const nextFocusIsOverlay = nextFocus ? this.overlayStack.some((entry) => entry.component === nextFocus) : false;
        const restoreState = this.getVisibleOverlayFocusRestore();
        if (nextFocus && !nextFocusIsOverlay) {
            if (restoreState.status === "blocked" && restoreState.blockedBy === previousFocus) {
                if (restoreState.resume.status === "focus-target" || !this.isComponentMounted(restoreState.blockedBy)) {
                    nextFocus = this.resolveBlockedOverlayFocusResume(restoreState);
                }
                else {
                    this.overlayFocusRestore = {
                        status: "blocked",
                        overlay: restoreState.overlay,
                        blockedBy: nextFocus,
                        resume: restoreState.resume,
                    };
                }
            }
            else if (previousFocusedOverlay &&
                restoreState.status !== "inactive" &&
                restoreState.overlay === previousFocusedOverlay &&
                !this.isOverlayFocusAncestor(previousFocusedOverlay, nextFocus)) {
                this.overlayFocusRestore = {
                    status: "blocked",
                    overlay: previousFocusedOverlay,
                    blockedBy: nextFocus,
                    resume: { status: "restore-overlay" },
                };
            }
        }
        else if (nextFocus === null) {
            if (restoreState.status === "blocked" && restoreState.blockedBy === previousFocus) {
                nextFocus = this.resolveBlockedOverlayFocusResume(restoreState);
            }
            else if (overlayFocusRestore === "clear") {
                this.clearOverlayFocusRestore();
            }
        }
        if (isFocusable(this.focusedComponent)) {
            this.focusedComponent.focused = false;
        }
        this.focusedComponent = nextFocus;
        if (isFocusable(nextFocus)) {
            nextFocus.focused = true;
        }
        const focusedOverlay = nextFocus
            ? this.overlayStack.find((entry) => entry.component === nextFocus && this.isOverlayVisible(entry))
            : undefined;
        if (focusedOverlay) {
            this.overlayFocusRestore = { status: "eligible", overlay: focusedOverlay };
        }
    }
    clearOverlayFocusRestore() {
        this.overlayFocusRestore = { status: "inactive" };
    }
    clearOverlayFocusRestoreFor(overlay) {
        if (this.overlayFocusRestore.status !== "inactive" && this.overlayFocusRestore.overlay === overlay) {
            this.clearOverlayFocusRestore();
        }
    }
    resolveBlockedOverlayFocusResume(restoreState) {
        if (restoreState.resume.status === "restore-overlay")
            return restoreState.overlay.component;
        this.clearOverlayFocusRestore();
        return restoreState.resume.target;
    }
    getVisibleOverlayFocusRestore() {
        const restoreState = this.overlayFocusRestore;
        if (restoreState.status === "inactive")
            return restoreState;
        if (!this.overlayStack.includes(restoreState.overlay) || !this.isOverlayVisible(restoreState.overlay)) {
            return { status: "inactive" };
        }
        return restoreState;
    }
    isOverlayFocusAncestor(entry, component) {
        const visited = new Set();
        let current = entry.preFocus;
        while (current && !visited.has(current)) {
            visited.add(current);
            if (current === component)
                return true;
            current = this.overlayStack.find((overlay) => overlay.component === current)?.preFocus ?? null;
        }
        return false;
    }
    retargetOverlayPreFocus(removed) {
        for (const overlay of this.overlayStack) {
            if (overlay !== removed && overlay.preFocus === removed.component) {
                overlay.preFocus = removed.preFocus;
            }
        }
    }
    getMountedRoots() {
        return this.children;
    }
    isComponentMounted(component) {
        return this.getMountedRoots().some((child) => this.containsComponent(child, component));
    }
    containsComponent(root, target) {
        if (root === target)
            return true;
        if (!(root instanceof Container))
            return false;
        return root.children.some((child) => this.containsComponent(child, target));
    }
    /**
     * Show an overlay component with configurable positioning and sizing.
     * Returns a handle to control the overlay's visibility.
     */
    showOverlay(component, options) {
        const entry = {
            component,
            ...(options === undefined ? {} : { options }),
            preFocus: this.focusedComponent,
            hidden: false,
            focusOrder: ++this.focusOrderCounter,
        };
        this.overlayStack.push(entry);
        // Only focus if overlay is actually visible
        if (!options?.nonCapturing && this.isOverlayVisible(entry)) {
            this.setFocus(component);
        }
        this.terminal.hideCursor();
        this.requestRender();
        // Return handle for controlling this overlay
        return {
            hide: () => {
                const index = this.overlayStack.indexOf(entry);
                if (index !== -1) {
                    this.clearOverlayFocusRestoreFor(entry);
                    this.retargetOverlayPreFocus(entry);
                    this.overlayStack.splice(index, 1);
                    // Restore focus if this overlay had focus
                    if (this.focusedComponent === component) {
                        const topVisible = this.getTopmostVisibleOverlay();
                        this.setFocus(topVisible?.component ?? entry.preFocus);
                    }
                    if (this.overlayStack.length === 0)
                        this.terminal.hideCursor();
                    this.requestRender();
                }
            },
            setHidden: (hidden) => {
                if (entry.hidden === hidden)
                    return;
                entry.hidden = hidden;
                // Update focus when hiding/showing
                if (hidden) {
                    this.clearOverlayFocusRestoreFor(entry);
                    // If this overlay had focus, move focus to next visible or preFocus
                    if (this.focusedComponent === component) {
                        const topVisible = this.getTopmostVisibleOverlay();
                        this.setFocus(topVisible?.component ?? entry.preFocus);
                    }
                }
                else {
                    // Restore focus to this overlay when showing (if it's actually visible)
                    if (!options?.nonCapturing && this.isOverlayVisible(entry)) {
                        entry.focusOrder = ++this.focusOrderCounter;
                        this.setFocus(component);
                    }
                }
                this.requestRender();
            },
            isHidden: () => entry.hidden,
            focus: () => {
                if (!this.overlayStack.includes(entry) || !this.isOverlayVisible(entry))
                    return;
                entry.focusOrder = ++this.focusOrderCounter;
                this.setFocus(component);
                this.requestRender();
            },
            unfocus: (unfocusOptions) => {
                const isFocused = this.focusedComponent === component;
                const restoreState = this.overlayFocusRestore;
                const hasPendingRestore = restoreState.status !== "inactive" && restoreState.overlay === entry;
                if (!isFocused && !hasPendingRestore)
                    return;
                if (restoreState.status === "blocked" &&
                    restoreState.overlay === entry &&
                    this.focusedComponent === restoreState.blockedBy) {
                    if (unfocusOptions) {
                        this.overlayFocusRestore = {
                            status: "blocked",
                            overlay: entry,
                            blockedBy: restoreState.blockedBy,
                            resume: { status: "focus-target", target: unfocusOptions.target },
                        };
                    }
                    else {
                        this.clearOverlayFocusRestore();
                    }
                    this.requestRender();
                    return;
                }
                this.clearOverlayFocusRestoreFor(entry);
                if (isFocused || unfocusOptions) {
                    const topVisible = this.getTopmostVisibleOverlay();
                    const fallbackTarget = topVisible && topVisible !== entry ? topVisible.component : entry.preFocus;
                    this.setFocus(unfocusOptions ? unfocusOptions.target : fallbackTarget);
                }
                this.requestRender();
            },
            isFocused: () => this.focusedComponent === component,
        };
    }
    /** Hide the topmost overlay and restore previous focus. */
    hideOverlay() {
        const overlay = this.overlayStack[this.overlayStack.length - 1];
        if (!overlay)
            return;
        this.clearOverlayFocusRestoreFor(overlay);
        this.retargetOverlayPreFocus(overlay);
        this.overlayStack.pop();
        if (this.focusedComponent === overlay.component) {
            // Find topmost visible overlay, or fall back to preFocus
            const topVisible = this.getTopmostVisibleOverlay();
            this.setFocus(topVisible?.component ?? overlay.preFocus);
        }
        if (this.overlayStack.length === 0)
            this.terminal.hideCursor();
        this.requestRender();
    }
    /** Check if there are any visible overlays */
    hasOverlay() {
        return this.overlayStack.some((o) => this.isOverlayVisible(o));
    }
    /** Check if the focused component is a visible overlay */
    isOverlayFocused() {
        return this.overlayStack.some((entry) => entry.component === this.focusedComponent && this.isOverlayVisible(entry));
    }
    /** Check if an overlay entry is currently visible */
    isOverlayVisible(entry) {
        if (entry.hidden)
            return false;
        if (entry.options?.visible) {
            return entry.options.visible(this.terminal.columns, this.terminal.rows);
        }
        return true;
    }
    /** Find the visual-frontmost visible capturing overlay, if any */
    getTopmostVisibleOverlay() {
        let topmost;
        for (const overlay of this.overlayStack) {
            if (overlay.options?.nonCapturing || !this.isOverlayVisible(overlay))
                continue;
            if (!topmost || overlay.focusOrder > topmost.focusOrder) {
                topmost = overlay;
            }
        }
        return topmost;
    }
    invalidate() {
        for (const root of this.getMountedRoots())
            root.invalidate();
        for (const overlay of this.overlayStack)
            overlay.component.invalidate();
    }
    start() {
        this.stopped = false;
        this.beforeTerminalStart();
        this.terminal.start((data) => this.handleTerminalInput(data), () => this.requestRender());
        this.afterTerminalStart();
        this.terminal.hideCursor();
        if (this.terminalColorSchemeNotificationsEnabled) {
            this.terminal.write("\x1b[?2031h");
        }
        this.queryCellSize();
        this.requestRender();
    }
    addInputListener(listener) {
        this.inputListeners.add(listener);
        return () => {
            this.inputListeners.delete(listener);
        };
    }
    removeInputListener(listener) {
        this.inputListeners.delete(listener);
    }
    onTerminalColorSchemeChange(listener) {
        this.terminalColorSchemeListeners.add(listener);
        return () => {
            this.terminalColorSchemeListeners.delete(listener);
        };
    }
    setTerminalColorSchemeNotifications(enabled) {
        if (this.terminalColorSchemeNotificationsEnabled === enabled) {
            return;
        }
        this.terminalColorSchemeNotificationsEnabled = enabled;
        if (!this.stopped) {
            this.terminal.write(enabled ? "\x1b[?2031h" : "\x1b[?2031l");
        }
    }
    queryCellSize() {
        // Only query if terminal supports images (cell size is only used for image rendering)
        if (!getCapabilities().images) {
            return;
        }
        // Query terminal for cell size in pixels: CSI 16 t
        // Response format: CSI 6 ; height ; width t
        this.terminal.write("\x1b[16t");
    }
    stop(options = {}) {
        this.stopped = true;
        this.cancelRenderTimer();
        if (this.terminalColorSchemeNotificationsEnabled) {
            this.terminal.write("\x1b[?2031l");
        }
        this.beforeTerminalStop(options);
        this.terminal.showCursor();
        this.terminal.stop();
        this.afterTerminalStop(options);
    }
    renderNow(force = false) {
        if (force)
            this.resetRenderState();
        this.renderRequested = false;
        this.cancelRenderTimer();
        this.lastRenderAt = performance.now();
        this.doRender();
    }
    requestRender(force = false) {
        if (force) {
            this.resetRenderState();
            this.requestImmediateRender();
            return;
        }
        if (this.renderRequested)
            return;
        this.renderRequested = true;
        process.nextTick(() => this.scheduleRender());
    }
    requestImmediateRender() {
        this.cancelRenderTimer();
        this.renderRequested = true;
        if (this.immediateRenderScheduled)
            return;
        this.immediateRenderScheduled = true;
        process.nextTick(() => {
            this.immediateRenderScheduled = false;
            if (this.stopped || !this.renderRequested)
                return;
            // A previously queued scheduleRender() can create a timer before this
            // callback runs. User input must preempt that throttled frame.
            this.cancelRenderTimer();
            this.renderRequested = false;
            this.lastRenderAt = performance.now();
            this.doRender();
        });
    }
    cancelRenderTimer() {
        if (!this.renderTimer)
            return;
        clearTimeout(this.renderTimer);
        this.renderTimer = undefined;
    }
    scheduleRender() {
        if (this.stopped || this.renderTimer || !this.renderRequested) {
            return;
        }
        const elapsed = performance.now() - this.lastRenderAt;
        const delay = Math.max(0, TuiBase.MIN_RENDER_INTERVAL_MS - elapsed);
        this.renderTimer = setTimeout(() => {
            this.renderTimer = undefined;
            if (this.stopped || !this.renderRequested) {
                return;
            }
            this.renderRequested = false;
            this.lastRenderAt = performance.now();
            this.doRender();
            if (this.renderRequested) {
                this.scheduleRender();
            }
        }, delay);
    }
    handleTerminalInput(data) {
        if (this.consumeOsc11BackgroundResponse(data)) {
            return;
        }
        if (this.consumeTerminalColorSchemeReport(data)) {
            return;
        }
        if (this.inputListeners.size > 0) {
            let current = data;
            for (const listener of this.inputListeners) {
                const result = listener(current);
                if (result?.consume) {
                    return;
                }
                if (result?.data !== undefined) {
                    current = result.data;
                }
            }
            if (current.length === 0) {
                return;
            }
            data = current;
        }
        // Consume terminal cell size responses without blocking unrelated input.
        if (this.consumeCellSizeResponse(data)) {
            return;
        }
        // Global debug key handler (Shift+Ctrl+D)
        if (matchesKey(data, "shift+ctrl+d") && this.onDebug) {
            this.onDebug();
            return;
        }
        // If focused component is an overlay, verify it's still visible
        // (visibility can change due to terminal resize or visible() callback)
        const focusedOverlay = this.overlayStack.find((o) => o.component === this.focusedComponent);
        if (focusedOverlay && !this.isOverlayVisible(focusedOverlay)) {
            // Focused overlay is no longer visible, redirect to topmost visible overlay
            const topVisible = this.getTopmostVisibleOverlay();
            if (topVisible) {
                this.setFocus(topVisible.component);
            }
            else {
                this.setFocusInternal({ component: focusedOverlay.preFocus, overlayFocusRestore: "preserve" });
            }
        }
        const focusIsOverlay = this.overlayStack.some((o) => o.component === this.focusedComponent);
        if (!focusIsOverlay) {
            const restoreState = this.getVisibleOverlayFocusRestore();
            if (restoreState.status === "eligible") {
                this.setFocus(restoreState.overlay.component);
            }
            else if (restoreState.status === "blocked" && restoreState.blockedBy !== this.focusedComponent) {
                if (restoreState.resume.status === "restore-overlay") {
                    this.setFocus(restoreState.overlay.component);
                }
                else {
                    this.clearOverlayFocusRestore();
                    this.setFocus(restoreState.resume.target);
                }
            }
        }
        // Pass input to focused component (including Ctrl+C)
        // The focused component can decide how to handle Ctrl+C
        if (this.focusedComponent?.handleInput) {
            // Filter out key release events unless component opts in
            if (isKeyRelease(data) && !this.focusedComponent.wantsKeyRelease) {
                return;
            }
            this.focusedComponent.handleInput(data);
            // Keyboard input is latency-sensitive. Avoid the throttled timer path,
            // where even setTimeout(0) can take a full 16 ms tick on Windows.
            this.requestImmediateRender();
        }
    }
    consumeOsc11BackgroundResponse(data) {
        if (this.pendingOsc11BackgroundReplies <= 0) {
            return false;
        }
        if (!isOsc11BackgroundColorResponse(data)) {
            return false;
        }
        const rgb = parseOsc11BackgroundColor(data);
        this.pendingOsc11BackgroundReplies -= 1;
        const query = this.pendingOsc11BackgroundQueries.shift();
        if (query && !query.settled) {
            query.settled = true;
            if (query.timer) {
                clearTimeout(query.timer);
                query.timer = undefined;
            }
            query.resolve?.(rgb);
            query.resolve = undefined;
        }
        return true;
    }
    consumeTerminalColorSchemeReport(data) {
        const scheme = parseTerminalColorSchemeReport(data);
        if (!scheme) {
            return false;
        }
        for (const listener of this.terminalColorSchemeListeners) {
            listener(scheme);
        }
        return true;
    }
    consumeCellSizeResponse(data) {
        // Response format: ESC [ 6 ; height ; width t
        const match = data.match(/^\x1b\[6;(\d+);(\d+)t$/);
        if (!match) {
            return false;
        }
        const heightPx = parseInt(match[1], 10);
        const widthPx = parseInt(match[2], 10);
        if (heightPx <= 0 || widthPx <= 0) {
            return true;
        }
        setCellDimensions({ widthPx, heightPx });
        // Invalidate all components so images re-render with correct dimensions.
        this.invalidate();
        this.requestRender();
        return true;
    }
    /**
     * Resolve overlay layout from options.
     * Returns { width, row, col, maxHeight } for rendering.
     */
    resolveOverlayLayout(options, overlayHeight, termWidth, termHeight) {
        const opt = options ?? {};
        // Parse margin (clamp to non-negative)
        const margin = typeof opt.margin === "number"
            ? { top: opt.margin, right: opt.margin, bottom: opt.margin, left: opt.margin }
            : (opt.margin ?? {});
        const marginTop = Math.max(0, margin.top ?? 0);
        const marginRight = Math.max(0, margin.right ?? 0);
        const marginBottom = Math.max(0, margin.bottom ?? 0);
        const marginLeft = Math.max(0, margin.left ?? 0);
        // Available space after margins
        const availWidth = Math.max(1, termWidth - marginLeft - marginRight);
        const availHeight = Math.max(1, termHeight - marginTop - marginBottom);
        // === Resolve width ===
        let width = parseSizeValue(opt.width, termWidth) ?? Math.min(80, availWidth);
        // Apply minWidth
        if (opt.minWidth !== undefined) {
            width = Math.max(width, opt.minWidth);
        }
        // Clamp to available space
        width = Math.max(1, Math.min(width, availWidth));
        // === Resolve maxHeight ===
        let maxHeight = parseSizeValue(opt.maxHeight, termHeight);
        // Clamp to available space
        if (maxHeight !== undefined) {
            maxHeight = Math.max(1, Math.min(maxHeight, availHeight));
        }
        // Effective overlay height (may be clamped by maxHeight)
        const effectiveHeight = maxHeight !== undefined ? Math.min(overlayHeight, maxHeight) : overlayHeight;
        // === Resolve position ===
        let row;
        let col;
        if (opt.row !== undefined) {
            if (typeof opt.row === "string") {
                // Percentage: 0% = top, 100% = bottom (overlay stays within bounds)
                const match = opt.row.match(/^(\d+(?:\.\d+)?)%$/);
                if (match) {
                    const maxRow = Math.max(0, availHeight - effectiveHeight);
                    const percent = parseFloat(match[1]) / 100;
                    row = marginTop + Math.floor(maxRow * percent);
                }
                else {
                    // Invalid format, fall back to center
                    row = this.resolveAnchorRow("center", effectiveHeight, availHeight, marginTop);
                }
            }
            else {
                // Absolute row position
                row = opt.row;
            }
        }
        else {
            // Anchor-based (default: center)
            const anchor = opt.anchor ?? "center";
            row = this.resolveAnchorRow(anchor, effectiveHeight, availHeight, marginTop);
        }
        if (opt.col !== undefined) {
            if (typeof opt.col === "string") {
                // Percentage: 0% = left, 100% = right (overlay stays within bounds)
                const match = opt.col.match(/^(\d+(?:\.\d+)?)%$/);
                if (match) {
                    const maxCol = Math.max(0, availWidth - width);
                    const percent = parseFloat(match[1]) / 100;
                    col = marginLeft + Math.floor(maxCol * percent);
                }
                else {
                    // Invalid format, fall back to center
                    col = this.resolveAnchorCol("center", width, availWidth, marginLeft);
                }
            }
            else {
                // Absolute column position
                col = opt.col;
            }
        }
        else {
            // Anchor-based (default: center)
            const anchor = opt.anchor ?? "center";
            col = this.resolveAnchorCol(anchor, width, availWidth, marginLeft);
        }
        // Apply offsets
        if (opt.offsetY !== undefined)
            row += opt.offsetY;
        if (opt.offsetX !== undefined)
            col += opt.offsetX;
        // Clamp to terminal bounds (respecting margins)
        row = Math.max(marginTop, Math.min(row, termHeight - marginBottom - effectiveHeight));
        col = Math.max(marginLeft, Math.min(col, termWidth - marginRight - width));
        return { width, row, col, maxHeight };
    }
    resolveAnchorRow(anchor, height, availHeight, marginTop) {
        switch (anchor) {
            case "top-left":
            case "top-center":
            case "top-right":
                return marginTop;
            case "bottom-left":
            case "bottom-center":
            case "bottom-right":
                return marginTop + availHeight - height;
            case "left-center":
            case "center":
            case "right-center":
                return marginTop + Math.floor((availHeight - height) / 2);
        }
    }
    resolveAnchorCol(anchor, width, availWidth, marginLeft) {
        switch (anchor) {
            case "top-left":
            case "left-center":
            case "bottom-left":
                return marginLeft;
            case "top-right":
            case "right-center":
            case "bottom-right":
                return marginLeft + availWidth - width;
            case "top-center":
            case "center":
            case "bottom-center":
                return marginLeft + Math.floor((availWidth - width) / 2);
        }
    }
    /** Composite all overlays into content lines (sorted by focusOrder, higher = on top). */
    compositeOverlays(lines, termWidth, termHeight) {
        if (this.overlayStack.length === 0)
            return lines;
        const result = [...lines];
        // Pre-render all visible overlays and calculate positions
        const rendered = [];
        let minLinesNeeded = result.length;
        const visibleEntries = this.overlayStack.filter((e) => this.isOverlayVisible(e));
        visibleEntries.sort((a, b) => a.focusOrder - b.focusOrder);
        for (const entry of visibleEntries) {
            const { component, options } = entry;
            // Get layout with height=0 first to determine width and maxHeight
            // (width and maxHeight don't depend on overlay height)
            const { width, maxHeight } = this.resolveOverlayLayout(options, 0, termWidth, termHeight);
            // Render component at calculated width
            let overlayLines = component.render(width);
            // Apply maxHeight if specified
            if (maxHeight !== undefined && overlayLines.length > maxHeight) {
                overlayLines = overlayLines.slice(0, maxHeight);
            }
            // Get final row/col with actual overlay height
            const { row, col } = this.resolveOverlayLayout(options, overlayLines.length, termWidth, termHeight);
            rendered.push({ overlayLines, row, col, w: width });
            minLinesNeeded = Math.max(minLinesNeeded, row + overlayLines.length);
        }
        // Pad to at least terminal height so overlays have screen-relative positions.
        // Excludes maxLinesRendered: the historical high-water mark caused self-reinforcing
        // inflation that pushed content into scrollback on terminal widen.
        const workingHeight = Math.max(result.length, termHeight, minLinesNeeded);
        // Extend result with empty lines if content is too short for overlay placement or working area
        while (result.length < workingHeight) {
            result.push("");
        }
        const viewportStart = Math.max(0, workingHeight - termHeight);
        // Composite each overlay
        for (const { overlayLines, row, col, w } of rendered) {
            for (let i = 0; i < overlayLines.length; i++) {
                const idx = viewportStart + row + i;
                if (idx >= 0 && idx < result.length) {
                    // Defensive: truncate overlay line to declared width before compositing
                    // (components should already respect width, but this ensures it)
                    const truncatedOverlayLine = visibleWidth(overlayLines[i]) > w ? sliceByColumn(overlayLines[i], 0, w, true) : overlayLines[i];
                    result[idx] = this.compositeLineAt(result[idx], truncatedOverlayLine, col, w, termWidth);
                }
            }
        }
        return result;
    }
    applyLineResets(lines) {
        const reset = SEGMENT_RESET;
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];
            if (!isImageLine(line)) {
                lines[i] = normalizeTerminalOutput(line) + reset;
            }
        }
        return lines;
    }
    compositeLineAt(baseLine, overlayLine, startCol, overlayWidth, totalWidth) {
        return compositeTuiLine(baseLine, overlayLine, startCol, overlayWidth, totalWidth);
    }
    /**
     * Find and extract cursor position from rendered lines.
     * Searches for CURSOR_MARKER, calculates its position, and strips it from the output.
     * Only scans the bottom terminal height lines (visible viewport).
     * @param lines - Rendered lines to search
     * @param height - Terminal height (visible viewport size)
     * @returns Cursor position { row, col } or null if no marker found
     */
    extractCursorPosition(lines, height) {
        // Only scan the bottom `height` lines (visible viewport)
        const viewportTop = Math.max(0, lines.length - height);
        for (let row = lines.length - 1; row >= viewportTop; row--) {
            const line = lines[row];
            const markerIndex = line.indexOf(CURSOR_MARKER);
            if (markerIndex !== -1) {
                // Calculate visual column (width of text before marker)
                const beforeMarker = line.slice(0, markerIndex);
                const col = visibleWidth(beforeMarker);
                // Strip marker from the line
                lines[row] = line.slice(0, markerIndex) + line.slice(markerIndex + CURSOR_MARKER.length);
                return { row, col };
            }
        }
        return null;
    }
    /**
     * Query the terminal's default background color with OSC 11 (`ESC ] 11 ; ? BEL`).
     * @param timeoutMs Query timeout in milliseconds.
     * @returns Promise containing the parsed RGB color, or undefined if it times out or fails to parse.
     */
    queryTerminalBackgroundColor({ timeoutMs }) {
        return new Promise((resolve) => {
            const query = {
                settled: false,
                resolve,
                timer: undefined,
            };
            query.timer = setTimeout(() => {
                if (query.settled) {
                    return;
                }
                query.settled = true;
                query.timer = undefined;
                query.resolve?.(undefined);
                query.resolve = undefined;
            }, timeoutMs);
            this.pendingOsc11BackgroundQueries.push(query);
            this.pendingOsc11BackgroundReplies += 1;
            this.terminal.write("\x1b]11;?\x07");
        });
    }
    /**
     * Query the terminal's color-scheme preference with DSR (`CSI ? 996 n`).
     * Terminals that support the color palette notification protocol reply with
     * `CSI ? 997 ; 1 n` for dark or `CSI ? 997 ; 2 n` for light.
     */
    queryTerminalColorScheme({ timeoutMs }) {
        return new Promise((resolve) => {
            let settled = false;
            let timer;
            let unsubscribe = () => { };
            const settle = (scheme) => {
                if (settled)
                    return;
                settled = true;
                if (timer) {
                    clearTimeout(timer);
                    timer = undefined;
                }
                unsubscribe();
                resolve(scheme);
            };
            unsubscribe = this.onTerminalColorSchemeChange(settle);
            timer = setTimeout(() => settle(undefined), timeoutMs);
            this.terminal.write("\x1b[?996n");
        });
    }
}
//# sourceMappingURL=tui.js.map