import { visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
import { ROW_INTENT_META } from "../../state/row-intent.js";
export class WrappingSelect {
    static { this.ACTIVE_POINTER = "❯ "; }
    static { this.INACTIVE_POINTER = "  "; }
    static { this.NUMBER_SEPARATOR = ". "; }
    static { this.CURSOR_INVERSE_START = "\x1b[7m"; }
    static { this.CURSOR_INVERSE_END = "\x1b[0m"; }
    static { this.CONFIRMED_MARK = " ✔"; }
    static { this.MIN_CONTENT_WIDTH = 1; }
    constructor(items, maxVisible, theme, options = {}) {
        this.selectedIndex = 0;
        this.focused = true;
        this.inputBuffer = "";
        this.inputCursor = undefined;
        /**
         * Index of the row that was previously confirmed for this list (e.g. the user's prior
         * answer when re-entering a multi-question tab). Renders `<label> ✔` in the active-row
         * styling but WITHOUT the `❯` pointer — pointer is reserved for the live cursor. When
         * `selectedIndex === confirmedIndex && focused`, the active rendering wins (no double-mark).
         */
        this.confirmedIndex = undefined;
        /**
         * When set together with `confirmedIndex`, replaces the row's static label at render time.
         * Used for the `kind: "other"` sentinel — its label is "Type something." but if the user's
         * prior answer was custom text, we render that text instead (e.g. `4. Hello ✔`).
         */
        this.confirmedLabelOverride = undefined;
        this.items = items;
        this.maxVisible = Math.max(1, maxVisible);
        this.theme = theme;
        this.numberStartOffset = options.numberStartOffset ?? 0;
        this.totalItemsForNumbering = options.totalItemsForNumbering ?? items.length;
    }
    /**
     * Update the numbering offset + total padding width without rebuilding the component.
     * Used by the host to keep the chat-row WrappingSelect's number aligned with the active tab's
     * options list when the user switches tabs (each tab can have a different items count).
     */
    setNumbering(numberStartOffset, totalItemsForNumbering) {
        this.numberStartOffset = numberStartOffset;
        this.totalItemsForNumbering = Math.max(1, totalItemsForNumbering);
    }
    setSelectedIndex(index) {
        this.selectedIndex = Math.max(0, Math.min(index, this.items.length - 1));
    }
    setFocused(focused) {
        this.focused = focused;
    }
    /**
     * Mark a previously-confirmed row. Pass `undefined` to clear. `labelOverride` replaces
     * the row's static `item.label` at render time — used for the `kind: "other"` sentinel so
     * the row reads `Hello ✔` instead of `Type something. ✔` when the prior answer was custom
     * text.
     */
    setConfirmedIndex(index, labelOverride) {
        if (index === undefined) {
            this.confirmedIndex = undefined;
            this.confirmedLabelOverride = undefined;
            return;
        }
        this.confirmedIndex = Math.max(0, Math.min(index, this.items.length - 1));
        this.confirmedLabelOverride = labelOverride;
    }
    setInputBuffer(text) {
        this.inputBuffer = text;
        if (this.inputCursor !== undefined)
            this.inputCursor = this.clampInputCursor(this.inputCursor);
    }
    setInputCursor(cursor) {
        this.inputCursor = this.clampInputCursor(cursor);
    }
    /** Intentionally empty — input is routed at the container level. */
    handleInput(_data) { }
    invalidate() { }
    render(width) {
        if (this.items.length === 0)
            return [];
        const { startIndex, endIndex } = this.computeVisibleWindow();
        const numberWidth = String(Math.max(1, this.totalItemsForNumbering)).length;
        const lines = [];
        for (let i = startIndex; i < endIndex; i++) {
            const item = this.items[i];
            if (!item)
                continue;
            const isActive = i === this.selectedIndex && this.focused;
            lines.push(...this.renderItem(item, i, isActive, width, numberWidth));
        }
        if (this.hasItemsOutsideWindow(startIndex, endIndex)) {
            lines.push(this.theme.scrollInfo(`  (${this.selectedIndex + 1}/${this.items.length})`));
        }
        return lines;
    }
    computeVisibleWindow() {
        const half = Math.floor(this.maxVisible / 2);
        const startIndex = Math.max(0, Math.min(this.selectedIndex - half, this.items.length - this.maxVisible));
        const endIndex = Math.min(startIndex + this.maxVisible, this.items.length);
        return { startIndex, endIndex };
    }
    hasItemsOutsideWindow(startIndex, endIndex) {
        return startIndex > 0 || endIndex < this.items.length;
    }
    renderItem(item, index, isActive, width, numberWidth) {
        const rowPrefix = this.buildRowPrefix(index, isActive, numberWidth);
        const continuationPrefix = " ".repeat(visibleWidth(rowPrefix));
        const contentWidth = Math.max(WrappingSelect.MIN_CONTENT_WIDTH, width - visibleWidth(rowPrefix));
        if (this.shouldRenderAsInlineInput(item, isActive)) {
            return this.renderInlineInputRow(rowPrefix, continuationPrefix, contentWidth);
        }
        // Confirmed row gets a trailing ` ✔` and accent+bold styling; pointer is independent
        // (still ❯ when active). When `index === confirmedIndex` AND `isActive`, both `❯` and
        // `✔` appear on the same row — load-bearing for the case where the prior answer was
        // row 0 (cursor resets to 0 on tab-back, so the confirmed row IS the active row).
        // Optional `confirmedLabelOverride` replaces the static label (used for `kind: "other"`
        // + `kind: "custom"` answer); the inline-input branch above still wins for `kind: "other" + isActive`.
        const isConfirmed = index === this.confirmedIndex;
        const label = isConfirmed
            ? `${this.confirmedLabelOverride ?? item.label}${WrappingSelect.CONFIRMED_MARK}`
            : item.label;
        const applySelectedStyle = isActive || isConfirmed;
        return [
            ...this.renderLabelBlock(label, rowPrefix, continuationPrefix, contentWidth, applySelectedStyle),
            ...this.renderDescriptionBlock(item.description, continuationPrefix, contentWidth),
        ];
    }
    buildRowPrefix(index, isActive, numberWidth) {
        const pointer = isActive ? WrappingSelect.ACTIVE_POINTER : WrappingSelect.INACTIVE_POINTER;
        const displayNumber = this.numberStartOffset + index + 1;
        const paddedNumber = String(displayNumber).padStart(numberWidth, " ");
        return `${pointer}${paddedNumber}${WrappingSelect.NUMBER_SEPARATOR}`;
    }
    shouldRenderAsInlineInput(item, isActive) {
        return isActive && ROW_INTENT_META[item.kind].activatesInputMode;
    }
    /**
     * Render the inline input row across one or more lines, wrapping at `contentWidth`
     * so long input doesn't run off the right edge or trip the parent renderer's
     * width invariant. Mirrors `renderLabelBlock`'s contract: first line carries
     * `rowPrefix`, continuation lines carry `continuationPrefix` (spaces), and every
     * emitted line passes through `theme.selectedText`. The cursor mirrors the
     * main chat editor: inverse-video over the character at the caret, or an
     * inverse-video space at end-of-line.
     */
    renderInlineInputRow(rowPrefix, continuationPrefix, contentWidth) {
        const raw = this.inputTextWithCursor();
        const wrapped = wrapTextWithAnsi(raw, contentWidth);
        return wrapped.map((segment, index) => {
            const prefix = index === 0 ? rowPrefix : continuationPrefix;
            return this.theme.selectedText(`${prefix}${segment}`);
        });
    }
    inputTextWithCursor() {
        const cursor = this.clampInputCursor(this.inputCursor ?? this.inputBuffer.length);
        const before = this.inputBuffer.slice(0, cursor);
        const after = this.inputBuffer.slice(cursor);
        const [at = ""] = Array.from(after);
        if (at === "") {
            return `${before}${WrappingSelect.CURSOR_INVERSE_START} ${WrappingSelect.CURSOR_INVERSE_END}`;
        }
        const rest = after.slice(at.length);
        return `${before}${WrappingSelect.CURSOR_INVERSE_START}${at}${WrappingSelect.CURSOR_INVERSE_END}${rest}`;
    }
    clampInputCursor(cursor) {
        if (!Number.isFinite(cursor))
            return this.inputBuffer.length;
        return Math.max(0, Math.min(cursor, this.inputBuffer.length));
    }
    renderLabelBlock(label, rowPrefix, continuationPrefix, contentWidth, applySelectedStyle) {
        const wrapped = wrapTextWithAnsi(label, contentWidth);
        return wrapped.map((segment, index) => {
            const prefix = index === 0 ? rowPrefix : continuationPrefix;
            const line = `${prefix}${segment}`;
            return applySelectedStyle ? this.theme.selectedText(line) : line;
        });
    }
    renderDescriptionBlock(description, continuationPrefix, contentWidth) {
        if (!description)
            return [];
        const wrapped = wrapTextWithAnsi(description, contentWidth);
        return wrapped.map((segment) => `${continuationPrefix}${this.theme.description(segment)}`);
    }
}
//# sourceMappingURL=wrapping-select.js.map