import { getKeybindings } from "../keybindings.js";
import { decodeKittyPrintable } from "../keys.js";
import { KillRing } from "../kill-ring.js";
import { CURSOR_MARKER } from "../tui.js";
import { UndoStack } from "../undo-stack.js";
import { getGraphemeSegmenter, isWhitespaceChar, sliceByColumn, visibleWidth } from "../utils.js";
import { findWordBackward, findWordForward } from "../word-navigation.js";
const segmenter = getGraphemeSegmenter();
/**
 * Input component - single-line text input with horizontal scrolling
 */
export class Input {
    value = "";
    cursor = 0; // Cursor position in the value
    onSubmit;
    onEscape;
    /** Focusable interface - set by TUI when focus changes */
    focused = false;
    // Bracketed paste mode buffering
    pasteBuffer = "";
    isInPaste = false;
    // Kill ring for Emacs-style kill/yank operations
    killRing = new KillRing();
    lastAction = null;
    // Undo support
    undoStack = new UndoStack();
    getValue() {
        return this.value;
    }
    setValue(value) {
        this.value = value;
        this.cursor = Math.min(this.cursor, value.length);
    }
    handleInput(data) {
        // Handle bracketed paste mode
        // Start of paste: \x1b[200~
        // End of paste: \x1b[201~
        // Check if we're starting a bracketed paste
        if (data.includes("\x1b[200~")) {
            this.isInPaste = true;
            this.pasteBuffer = "";
            data = data.replace("\x1b[200~", "");
        }
        // If we're in a paste, buffer the data
        if (this.isInPaste) {
            // Check if this chunk contains the end marker
            this.pasteBuffer += data;
            const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
            if (endIndex !== -1) {
                // Extract the pasted content
                const pasteContent = this.pasteBuffer.substring(0, endIndex);
                // Process the complete paste
                this.handlePaste(pasteContent);
                // Reset paste state
                this.isInPaste = false;
                // Handle any remaining input after the paste marker
                const remaining = this.pasteBuffer.substring(endIndex + 6); // 6 = length of \x1b[201~
                this.pasteBuffer = "";
                if (remaining) {
                    this.handleInput(remaining);
                }
            }
            return;
        }
        const kb = getKeybindings();
        // Escape/Cancel
        if (kb.matches(data, "tui.select.cancel")) {
            if (this.onEscape)
                this.onEscape();
            return;
        }
        // Undo
        if (kb.matches(data, "tui.editor.undo")) {
            this.undo();
            return;
        }
        // Submit
        if (kb.matches(data, "tui.input.submit") || data === "\n") {
            if (this.onSubmit)
                this.onSubmit(this.value);
            return;
        }
        // Deletion
        if (kb.matches(data, "tui.editor.deleteCharBackward")) {
            this.handleBackspace();
            return;
        }
        if (kb.matches(data, "tui.editor.deleteCharForward")) {
            this.handleForwardDelete();
            return;
        }
        if (kb.matches(data, "tui.editor.deleteWordBackward")) {
            this.deleteWordBackwards();
            return;
        }
        if (kb.matches(data, "tui.editor.deleteWordForward")) {
            this.deleteWordForward();
            return;
        }
        if (kb.matches(data, "tui.editor.deleteToLineStart")) {
            this.deleteToLineStart();
            return;
        }
        if (kb.matches(data, "tui.editor.deleteToLineEnd")) {
            this.deleteToLineEnd();
            return;
        }
        // Kill ring actions
        if (kb.matches(data, "tui.editor.yank")) {
            this.yank();
            return;
        }
        if (kb.matches(data, "tui.editor.yankPop")) {
            this.yankPop();
            return;
        }
        // Cursor movement
        if (kb.matches(data, "tui.editor.cursorLeft")) {
            this.lastAction = null;
            if (this.cursor > 0) {
                const beforeCursor = this.value.slice(0, this.cursor);
                const graphemes = [...segmenter.segment(beforeCursor)];
                const lastGrapheme = graphemes[graphemes.length - 1];
                this.cursor -= lastGrapheme ? lastGrapheme.segment.length : 1;
            }
            return;
        }
        if (kb.matches(data, "tui.editor.cursorRight")) {
            this.lastAction = null;
            if (this.cursor < this.value.length) {
                const afterCursor = this.value.slice(this.cursor);
                const graphemes = [...segmenter.segment(afterCursor)];
                const firstGrapheme = graphemes[0];
                this.cursor += firstGrapheme ? firstGrapheme.segment.length : 1;
            }
            return;
        }
        if (kb.matches(data, "tui.editor.cursorLineStart")) {
            this.lastAction = null;
            this.cursor = 0;
            return;
        }
        if (kb.matches(data, "tui.editor.cursorLineEnd")) {
            this.lastAction = null;
            this.cursor = this.value.length;
            return;
        }
        if (kb.matches(data, "tui.editor.cursorWordLeft")) {
            this.moveWordBackwards();
            return;
        }
        if (kb.matches(data, "tui.editor.cursorWordRight")) {
            this.moveWordForwards();
            return;
        }
        // Kitty CSI-u printable character (e.g. \x1b[97u for 'a').
        // Terminals with Kitty protocol flag 1 (disambiguate) send CSI-u for all keys,
        // including plain printable characters. Decode before the control-char check
        // since CSI-u sequences contain \x1b which would be rejected.
        const kittyPrintable = decodeKittyPrintable(data);
        if (kittyPrintable !== undefined) {
            this.insertCharacter(kittyPrintable);
            return;
        }
        // Regular character input - accept printable characters including Unicode,
        // but reject control characters (C0: 0x00-0x1F, DEL: 0x7F, C1: 0x80-0x9F)
        const hasControlChars = [...data].some((ch) => {
            const code = ch.charCodeAt(0);
            return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
        });
        if (!hasControlChars) {
            this.insertCharacter(data);
        }
    }
    insertCharacter(char) {
        // Undo coalescing: consecutive word chars coalesce into one undo unit
        if (isWhitespaceChar(char) || this.lastAction !== "type-word") {
            this.pushUndo();
        }
        this.lastAction = "type-word";
        this.value = this.value.slice(0, this.cursor) + char + this.value.slice(this.cursor);
        this.cursor += char.length;
    }
    handleBackspace() {
        this.lastAction = null;
        if (this.cursor > 0) {
            this.pushUndo();
            const beforeCursor = this.value.slice(0, this.cursor);
            const graphemes = [...segmenter.segment(beforeCursor)];
            const lastGrapheme = graphemes[graphemes.length - 1];
            const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
            this.value = this.value.slice(0, this.cursor - graphemeLength) + this.value.slice(this.cursor);
            this.cursor -= graphemeLength;
        }
    }
    handleForwardDelete() {
        this.lastAction = null;
        if (this.cursor < this.value.length) {
            this.pushUndo();
            const afterCursor = this.value.slice(this.cursor);
            const graphemes = [...segmenter.segment(afterCursor)];
            const firstGrapheme = graphemes[0];
            const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
            this.value = this.value.slice(0, this.cursor) + this.value.slice(this.cursor + graphemeLength);
        }
    }
    deleteToLineStart() {
        if (this.cursor === 0)
            return;
        this.pushUndo();
        const deletedText = this.value.slice(0, this.cursor);
        this.killRing.push(deletedText, { prepend: true, accumulate: this.lastAction === "kill" });
        this.lastAction = "kill";
        this.value = this.value.slice(this.cursor);
        this.cursor = 0;
    }
    deleteToLineEnd() {
        if (this.cursor >= this.value.length)
            return;
        this.pushUndo();
        const deletedText = this.value.slice(this.cursor);
        this.killRing.push(deletedText, { prepend: false, accumulate: this.lastAction === "kill" });
        this.lastAction = "kill";
        this.value = this.value.slice(0, this.cursor);
    }
    deleteWordBackwards() {
        if (this.cursor === 0)
            return;
        // Save lastAction before cursor movement (moveWordBackwards resets it)
        const wasKill = this.lastAction === "kill";
        this.pushUndo();
        const oldCursor = this.cursor;
        this.moveWordBackwards();
        const deleteFrom = this.cursor;
        this.cursor = oldCursor;
        const deletedText = this.value.slice(deleteFrom, this.cursor);
        this.killRing.push(deletedText, { prepend: true, accumulate: wasKill });
        this.lastAction = "kill";
        this.value = this.value.slice(0, deleteFrom) + this.value.slice(this.cursor);
        this.cursor = deleteFrom;
    }
    deleteWordForward() {
        if (this.cursor >= this.value.length)
            return;
        // Save lastAction before cursor movement (moveWordForwards resets it)
        const wasKill = this.lastAction === "kill";
        this.pushUndo();
        const oldCursor = this.cursor;
        this.moveWordForwards();
        const deleteTo = this.cursor;
        this.cursor = oldCursor;
        const deletedText = this.value.slice(this.cursor, deleteTo);
        this.killRing.push(deletedText, { prepend: false, accumulate: wasKill });
        this.lastAction = "kill";
        this.value = this.value.slice(0, this.cursor) + this.value.slice(deleteTo);
    }
    yank() {
        const text = this.killRing.peek();
        if (!text)
            return;
        this.pushUndo();
        this.value = this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor);
        this.cursor += text.length;
        this.lastAction = "yank";
    }
    yankPop() {
        if (this.lastAction !== "yank" || this.killRing.length <= 1)
            return;
        this.pushUndo();
        // Delete the previously yanked text (still at end of ring before rotation)
        const prevText = this.killRing.peek() || "";
        this.value = this.value.slice(0, this.cursor - prevText.length) + this.value.slice(this.cursor);
        this.cursor -= prevText.length;
        // Rotate and insert new entry
        this.killRing.rotate();
        const text = this.killRing.peek() || "";
        this.value = this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor);
        this.cursor += text.length;
        this.lastAction = "yank";
    }
    pushUndo() {
        this.undoStack.push({ value: this.value, cursor: this.cursor });
    }
    undo() {
        const snapshot = this.undoStack.pop();
        if (!snapshot)
            return;
        this.value = snapshot.value;
        this.cursor = snapshot.cursor;
        this.lastAction = null;
    }
    moveWordBackwards() {
        if (this.cursor === 0)
            return;
        this.lastAction = null;
        this.cursor = findWordBackward(this.value, this.cursor);
    }
    moveWordForwards() {
        if (this.cursor >= this.value.length)
            return;
        this.lastAction = null;
        this.cursor = findWordForward(this.value, this.cursor);
    }
    handlePaste(pastedText) {
        this.lastAction = null;
        this.pushUndo();
        // Clean the pasted text - remove newlines and carriage returns
        const cleanText = pastedText.replace(/\r\n/g, "").replace(/\r/g, "").replace(/\n/g, "").replace(/\t/g, "    ");
        // Insert at cursor position
        this.value = this.value.slice(0, this.cursor) + cleanText + this.value.slice(this.cursor);
        this.cursor += cleanText.length;
    }
    invalidate() {
        // No cached state to invalidate currently
    }
    render(width) {
        // Calculate visible window
        const prompt = "> ";
        const availableWidth = width - prompt.length;
        if (availableWidth <= 0) {
            return [prompt];
        }
        let visibleText = "";
        let cursorDisplay = this.cursor;
        const totalWidth = visibleWidth(this.value);
        if (totalWidth < availableWidth) {
            // Everything fits (leave room for cursor at end)
            visibleText = this.value;
        }
        else {
            // Need horizontal scrolling
            // Reserve one column for cursor if it's at the end
            const scrollWidth = this.cursor === this.value.length ? availableWidth - 1 : availableWidth;
            const cursorCol = visibleWidth(this.value.slice(0, this.cursor));
            if (scrollWidth > 0) {
                const halfWidth = Math.floor(scrollWidth / 2);
                let startCol = 0;
                if (cursorCol < halfWidth) {
                    // Cursor near start
                    startCol = 0;
                }
                else if (cursorCol > totalWidth - halfWidth) {
                    // Cursor near end
                    startCol = Math.max(0, totalWidth - scrollWidth);
                }
                else {
                    // Cursor in middle
                    startCol = Math.max(0, cursorCol - halfWidth);
                }
                visibleText = sliceByColumn(this.value, startCol, scrollWidth, true);
                const beforeCursor = sliceByColumn(this.value, startCol, Math.max(0, cursorCol - startCol), true);
                cursorDisplay = beforeCursor.length;
            }
            else {
                visibleText = "";
                cursorDisplay = 0;
            }
        }
        // Build line with fake cursor
        // Insert cursor character at cursor position
        const graphemes = [...segmenter.segment(visibleText.slice(cursorDisplay))];
        const cursorGrapheme = graphemes[0];
        const beforeCursor = visibleText.slice(0, cursorDisplay);
        const atCursor = cursorGrapheme?.segment ?? " "; // Character at cursor, or space if at end
        const afterCursor = visibleText.slice(cursorDisplay + atCursor.length);
        // Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning)
        const marker = this.focused ? CURSOR_MARKER : "";
        // Use inverse video to show cursor
        const cursorChar = `\x1b[7m${atCursor}\x1b[27m`; // ESC[7m = reverse video, ESC[27m = normal
        const textWithCursor = beforeCursor + marker + cursorChar + afterCursor;
        // Calculate visual width
        const visualLength = visibleWidth(textWithCursor);
        const padding = " ".repeat(Math.max(0, availableWidth - visualLength));
        const line = prompt + textWithCursor + padding;
        return [line];
    }
}
//# sourceMappingURL=input.js.map