import { isInputModeCharacter } from '../components/PromptInput/inputModes.js';
import { useNotifications } from '../context/notifications.js';
import stripAnsi from 'strip-ansi';
import { markBackslashReturnUsed } from '../commands/terminalSetup/terminalSetup.js';
import { addToHistory } from '../history.js';
import { Cursor, getLastKill, pushToKillRing, recordYank, resetKillAccumulation, resetYankState, updateYankLength, yankPop, } from '../utils/Cursor.js';
import { env } from '../utils/env.js';
import { isFullscreenEnvEnabled } from '../utils/fullscreen.js';
import { isModifierPressed, prewarmModifiers } from '../utils/modifiers.js';
import { useDoublePress } from './useDoublePress.js';
const NOOP_HANDLER = () => { };
function mapInput(input_map) {
    const map = new Map(input_map);
    return function (input) {
        return (map.get(input) ?? NOOP_HANDLER)(input);
    };
}
export function useTextInput({ value: originalValue, onChange, onSubmit, onExit, onExitMessage, onHistoryUp, onHistoryDown, onHistoryReset, onClearInput, mask = '', multiline = false, cursorChar, invert, columns, onImagePaste: _onImagePaste, disableCursorMovementForUpDownKeys = false, disableEscapeDoublePress = false, maxVisibleLines, externalOffset, onOffsetChange, inputFilter, inlineGhostText, dim, }) {
    // Pre-warm the modifiers module for Apple Terminal (has internal guard, safe to call multiple times)
    if (env.terminal === 'Apple_Terminal') {
        prewarmModifiers();
    }
    const offset = externalOffset;
    const setOffset = onOffsetChange;
    const cursor = Cursor.fromText(originalValue, columns, offset);
    const { addNotification, removeNotification } = useNotifications();
    const handleCtrlC = useDoublePress(show => {
        onExitMessage?.(show, 'Ctrl-C');
    }, () => onExit?.(), () => {
        if (originalValue) {
            onChange('');
            setOffset(0);
            onHistoryReset?.();
        }
    });
    // NOTE(keybindings): This escape handler is intentionally NOT migrated to the keybindings system.
    // It's a text-level double-press escape for clearing input, not an action-level keybinding.
    // Double-press Esc clears the input and saves to history - this is text editing behavior,
    // not dialog dismissal, and needs the double-press safety mechanism.
    const handleEscape = useDoublePress((show) => {
        if (!originalValue || !show) {
            return;
        }
        addNotification({
            key: 'escape-again-to-clear',
            text: 'Esc again to clear',
            priority: 'immediate',
            timeoutMs: 1000,
        });
    }, () => {
        // Remove the "Esc again to clear" notification immediately
        removeNotification('escape-again-to-clear');
        onClearInput?.();
        if (originalValue) {
            // Track double-escape usage for feature discovery
            // Save to history before clearing
            if (originalValue.trim() !== '') {
                addToHistory(originalValue);
            }
            onChange('');
            setOffset(0);
            onHistoryReset?.();
        }
    });
    const handleEmptyCtrlD = useDoublePress(show => {
        if (originalValue !== '') {
            return;
        }
        onExitMessage?.(show, 'Ctrl-D');
    }, () => {
        if (originalValue !== '') {
            return;
        }
        onExit?.();
    });
    function handleCtrlD() {
        if (cursor.text === '') {
            // When input is empty, handle double-press
            handleEmptyCtrlD();
            return cursor;
        }
        // When input is not empty, delete forward like iPython
        return cursor.del();
    }
    function killToLineEnd() {
        const { cursor: newCursor, killed } = cursor.deleteToLineEnd();
        pushToKillRing(killed, 'append');
        return newCursor;
    }
    function killToLineStart() {
        const { cursor: newCursor, killed } = cursor.deleteToLineStart();
        pushToKillRing(killed, 'prepend');
        return newCursor;
    }
    function killWordBefore() {
        const { cursor: newCursor, killed } = cursor.deleteWordBefore();
        pushToKillRing(killed, 'prepend');
        return newCursor;
    }
    function yank() {
        const text = getLastKill();
        if (text.length > 0) {
            const startOffset = cursor.offset;
            const newCursor = cursor.insert(text);
            recordYank(startOffset, text.length);
            return newCursor;
        }
        return cursor;
    }
    function handleYankPop() {
        const popResult = yankPop();
        if (!popResult) {
            return cursor;
        }
        const { text, start, length } = popResult;
        // Replace the previously yanked text with the new one
        const before = cursor.text.slice(0, start);
        const after = cursor.text.slice(start + length);
        const newText = before + text + after;
        const newOffset = start + text.length;
        updateYankLength(text.length);
        return Cursor.fromText(newText, columns, newOffset);
    }
    const handleCtrl = mapInput([
        ['a', () => cursor.startOfLine()],
        ['b', () => cursor.left()],
        ['c', handleCtrlC],
        ['d', handleCtrlD],
        ['e', () => cursor.endOfLine()],
        ['f', () => cursor.right()],
        ['h', () => cursor.deleteTokenBefore() ?? cursor.backspace()],
        ['k', killToLineEnd],
        ['n', () => downOrHistoryDown()],
        ['p', () => upOrHistoryUp()],
        ['u', killToLineStart],
        ['w', killWordBefore],
        ['y', yank],
    ]);
    const handleMeta = mapInput([
        ['b', () => cursor.prevWord()],
        ['f', () => cursor.nextWord()],
        ['d', () => cursor.deleteWordAfter()],
        ['y', handleYankPop],
    ]);
    function handleEnter(key) {
        if (multiline &&
            cursor.offset > 0 &&
            cursor.text[cursor.offset - 1] === '\\') {
            // Track that the user has used backslash+return
            markBackslashReturnUsed();
            return cursor.backspace().insert('\n');
        }
        // Meta+Enter or Shift+Enter inserts a newline
        if (key.meta || key.shift) {
            return cursor.insert('\n');
        }
        // Apple Terminal doesn't support custom Shift+Enter keybindings,
        // so we use native macOS modifier detection to check if Shift is held
        if (env.terminal === 'Apple_Terminal' && isModifierPressed('shift')) {
            return cursor.insert('\n');
        }
        onSubmit?.(originalValue);
    }
    function upOrHistoryUp() {
        if (disableCursorMovementForUpDownKeys) {
            onHistoryUp?.();
            return cursor;
        }
        // Try to move by wrapped lines first
        const cursorUp = cursor.up();
        if (!cursorUp.equals(cursor)) {
            return cursorUp;
        }
        // If we can't move by wrapped lines and this is multiline input,
        // try to move by logical lines (to handle paragraph boundaries)
        if (multiline) {
            const cursorUpLogical = cursor.upLogicalLine();
            if (!cursorUpLogical.equals(cursor)) {
                return cursorUpLogical;
            }
        }
        // Can't move up at all - trigger history navigation
        onHistoryUp?.();
        return cursor;
    }
    function downOrHistoryDown() {
        if (disableCursorMovementForUpDownKeys) {
            onHistoryDown?.();
            return cursor;
        }
        // Try to move by wrapped lines first
        const cursorDown = cursor.down();
        if (!cursorDown.equals(cursor)) {
            return cursorDown;
        }
        // If we can't move by wrapped lines and this is multiline input,
        // try to move by logical lines (to handle paragraph boundaries)
        if (multiline) {
            const cursorDownLogical = cursor.downLogicalLine();
            if (!cursorDownLogical.equals(cursor)) {
                return cursorDownLogical;
            }
        }
        // Can't move down at all - trigger history navigation
        onHistoryDown?.();
        return cursor;
    }
    function mapKey(key) {
        switch (true) {
            case key.escape:
                return () => {
                    // Skip when a keybinding context (e.g. Autocomplete) owns escape.
                    // useKeybindings can't shield us via stopImmediatePropagation —
                    // BaseTextInput's useInput registers first (child effects fire
                    // before parent effects), so this handler has already run by the
                    // time the keybinding's handler stops propagation.
                    if (disableEscapeDoublePress)
                        return cursor;
                    handleEscape();
                    // Return the current cursor unchanged - handleEscape manages state internally
                    return cursor;
                };
            case key.leftArrow && (key.ctrl || key.meta || key.fn):
                return () => cursor.prevWord();
            case key.rightArrow && (key.ctrl || key.meta || key.fn):
                return () => cursor.nextWord();
            case key.backspace:
                return key.meta || key.ctrl
                    ? killWordBefore
                    : () => cursor.deleteTokenBefore() ?? cursor.backspace();
            case key.delete:
                return key.meta ? killToLineEnd : () => cursor.del();
            case key.ctrl:
                return handleCtrl;
            case key.home:
                return () => cursor.startOfLine();
            case key.end:
                return () => cursor.endOfLine();
            case key.pageDown:
                // In fullscreen mode, PgUp/PgDn scroll the message viewport instead
                // of moving the cursor — no-op here, ScrollKeybindingHandler handles it.
                if (isFullscreenEnvEnabled()) {
                    return NOOP_HANDLER;
                }
                return () => cursor.endOfLine();
            case key.pageUp:
                if (isFullscreenEnvEnabled()) {
                    return NOOP_HANDLER;
                }
                return () => cursor.startOfLine();
            case key.wheelUp:
            case key.wheelDown:
                // Mouse wheel events only exist when fullscreen mouse tracking is on.
                // ScrollKeybindingHandler handles them; no-op here to avoid inserting
                // the raw SGR sequence as text.
                return NOOP_HANDLER;
            case key.return:
                // Must come before key.meta so Option+Return inserts newline
                return () => handleEnter(key);
            case key.meta:
                return handleMeta;
            case key.tab:
                return () => cursor;
            case key.upArrow && !key.shift:
                return upOrHistoryUp;
            case key.downArrow && !key.shift:
                return downOrHistoryDown;
            case key.leftArrow:
                return () => cursor.left();
            case key.rightArrow:
                return () => cursor.right();
            default: {
                return function (input) {
                    switch (true) {
                        // Home key
                        case input === '\x1b[H' || input === '\x1b[1~':
                            return cursor.startOfLine();
                        // End key
                        case input === '\x1b[F' || input === '\x1b[4~':
                            return cursor.endOfLine();
                        default: {
                            // Trailing \r after text is SSH-coalesced Enter ("o\r") —
                            // strip it so the Enter isn't inserted as content. Lone \r
                            // here is Alt+Enter leaking through (META_KEY_CODE_RE doesn't
                            // match \x1b\r) — leave it for the \r→\n below. Embedded \r
                            // is multi-line paste from a terminal without bracketed
                            // paste — convert to \n. Backslash+\r is a stale VS Code
                            // Shift+Enter binding (some legacy /terminal-setup wrote
                            // args.text "\\\r\n" to keybindings.json); keep the \r so
                            // it becomes \n below.
                            const text = stripAnsi(input)
                                // eslint-disable-next-line custom-rules/no-lookbehind-regex -- .replace(re, str) on 1-2 char keystrokes: no-match returns same string (Object.is), regex never runs
                                .replace(/(?<=[^\\\r\n])\r$/, '')
                                .replace(/\r/g, '\n');
                            if (cursor.isAtStart() && isInputModeCharacter(input)) {
                                return cursor.insert(text).left();
                            }
                            return cursor.insert(text);
                        }
                    }
                };
            }
        }
    }
    // Check if this is a kill command (Ctrl+K, Ctrl+U, Ctrl+W, or Meta+Backspace/Delete)
    function isKillKey(key, input) {
        if (key.ctrl && (input === 'k' || input === 'u' || input === 'w')) {
            return true;
        }
        if (key.meta && (key.backspace || key.delete)) {
            return true;
        }
        return false;
    }
    // Check if this is a yank command (Ctrl+Y or Alt+Y)
    function isYankKey(key, input) {
        return (key.ctrl || key.meta) && input === 'y';
    }
    function onInput(input, key) {
        // Note: Image paste shortcut (chat:imagePaste) is handled via useKeybindings in PromptInput
        // Apply filter if provided
        const filteredInput = inputFilter ? inputFilter(input, key) : input;
        // If the input was filtered out, do nothing
        if (filteredInput === '' && input !== '') {
            return;
        }
        // Fix Issue #1853: Filter DEL characters that interfere with backspace in SSH/tmux
        // In SSH/tmux environments, backspace generates both key events and raw DEL chars
        if (!key.backspace && !key.delete && input.includes('\x7f')) {
            const delCount = (input.match(/\x7f/g) || []).length;
            // Apply all DEL characters as backspace operations synchronously
            // Try to delete tokens first, fall back to character backspace
            let currentCursor = cursor;
            for (let i = 0; i < delCount; i++) {
                currentCursor =
                    currentCursor.deleteTokenBefore() ?? currentCursor.backspace();
            }
            // Update state once with the final result
            if (!cursor.equals(currentCursor)) {
                if (cursor.text !== currentCursor.text) {
                    onChange(currentCursor.text);
                }
                setOffset(currentCursor.offset);
            }
            resetKillAccumulation();
            resetYankState();
            return;
        }
        // Reset kill accumulation for non-kill keys
        if (!isKillKey(key, filteredInput)) {
            resetKillAccumulation();
        }
        // Reset yank state for non-yank keys (breaks yank-pop chain)
        if (!isYankKey(key, filteredInput)) {
            resetYankState();
        }
        const nextCursor = mapKey(key)(filteredInput);
        if (nextCursor) {
            if (!cursor.equals(nextCursor)) {
                if (cursor.text !== nextCursor.text) {
                    onChange(nextCursor.text);
                }
                setOffset(nextCursor.offset);
            }
            // SSH-coalesced Enter: on slow links, "o" + Enter can arrive as one
            // chunk "o\r". parseKeypress only matches s === '\r', so it hit the
            // default handler above (which stripped the trailing \r). Text with
            // exactly one trailing \r is coalesced Enter; lone \r is Alt+Enter
            // (newline); embedded \r is multi-line paste.
            if (filteredInput.length > 1 &&
                filteredInput.endsWith('\r') &&
                !filteredInput.slice(0, -1).includes('\r') &&
                // Backslash+CR is a stale VS Code Shift+Enter binding, not
                // coalesced Enter. See default handler above.
                filteredInput[filteredInput.length - 2] !== '\\') {
                onSubmit?.(nextCursor.text);
            }
        }
    }
    // Prepare ghost text for rendering - validate insertPosition matches current
    // cursor offset to prevent stale ghost text from a previous keystroke causing
    // a one-frame jitter (ghost text state is updated via useEffect after render)
    const ghostTextForRender = inlineGhostText && dim && inlineGhostText.insertPosition === offset
        ? { text: inlineGhostText.text, dim }
        : undefined;
    const cursorPos = cursor.getPosition();
    return {
        onInput,
        renderedValue: cursor.render(cursorChar, mask, invert, ghostTextForRender, maxVisibleLines),
        offset,
        setOffset,
        cursorLine: cursorPos.line - cursor.getViewportStartLine(maxVisibleLines),
        cursorColumn: cursorPos.column,
        viewportCharOffset: cursor.getViewportCharOffset(maxVisibleLines),
        viewportCharEnd: cursor.getViewportCharEnd(maxVisibleLines),
    };
}
//# sourceMappingURL=useTextInput.js.map