import { EventEmitter } from 'node:events';
import process from 'node:process';
import React, { useState, useRef, useCallback, useMemo, useEffect, } from 'react';
import cliCursor from 'cli-cursor';
import { createInputParser } from '../input-parser.js';
import AppContext from './AppContext.js';
import StdinContext from './StdinContext.js';
import StdoutContext from './StdoutContext.js';
import StderrContext from './StderrContext.js';
import FocusContext from './FocusContext.js';
import CursorContext from './CursorContext.js';
import ErrorBoundary from './ErrorBoundary.js';
const tab = '\t';
const shiftTab = '\u001B[Z';
const escape = '\u001B';
// Root component for all Ink apps
// It renders stdin and stdout contexts, so that children can access them if needed
// It also handles Ctrl+C exiting and cursor visibility
function App({ children, stdin, stdout, stderr, writeToStdout, writeToStderr, exitOnCtrlC, onExit, setCursorPosition, }) {
    const [isFocusEnabled, setIsFocusEnabled] = useState(true);
    const [activeFocusId, setActiveFocusId] = useState(undefined);
    // Focusables array is managed internally via setFocusables callback pattern
    // eslint-disable-next-line react/hook-use-state
    const [, setFocusables] = useState([]);
    // Track focusables count for tab navigation check (avoids stale closure)
    const focusablesCountRef = useRef(0);
    // Count how many components enabled raw mode to avoid disabling
    // raw mode until all components don't need it anymore
    const rawModeEnabledCount = useRef(0);
    // eslint-disable-next-line @typescript-eslint/naming-convention
    const internal_eventEmitter = useRef(new EventEmitter());
    // Each useInput hook adds a listener, so the count can legitimately exceed the default limit of 10.
    internal_eventEmitter.current.setMaxListeners(Infinity);
    // Store the currently attached readable listener to avoid stale closure issues
    const readableListenerRef = useRef(undefined);
    const inputParserRef = useRef(createInputParser());
    const pendingInputFlushRef = useRef(undefined);
    const clearPendingInputFlush = useCallback(() => {
        if (!pendingInputFlushRef.current) {
            return;
        }
        clearImmediate(pendingInputFlushRef.current);
        pendingInputFlushRef.current = undefined;
    }, []);
    // Determines if TTY is supported on the provided stdin
    const isRawModeSupported = stdin.isTTY;
    const detachReadableListener = useCallback(() => {
        if (!readableListenerRef.current) {
            return;
        }
        stdin.removeListener('readable', readableListenerRef.current);
        readableListenerRef.current = undefined;
    }, [stdin]);
    const disableRawMode = useCallback(() => {
        stdin.setRawMode(false);
        detachReadableListener();
        stdin.unref();
        rawModeEnabledCount.current = 0;
        inputParserRef.current.reset();
        clearPendingInputFlush();
    }, [stdin, detachReadableListener, clearPendingInputFlush]);
    const handleExit = useCallback((errorOrResult) => {
        if (isRawModeSupported && rawModeEnabledCount.current > 0) {
            disableRawMode();
        }
        onExit(errorOrResult);
    }, [isRawModeSupported, disableRawMode, onExit]);
    const handleInput = useCallback((input) => {
        // Exit on Ctrl+C
        // eslint-disable-next-line unicorn/no-hex-escape
        if (input === '\x03' && exitOnCtrlC) {
            handleExit();
            return;
        }
        // Reset focus when there's an active focused component on Esc
        if (input === escape) {
            setActiveFocusId(currentActiveFocusId => {
                if (currentActiveFocusId) {
                    return undefined;
                }
                return currentActiveFocusId;
            });
        }
    }, [exitOnCtrlC, handleExit]);
    const emitInput = useCallback((input) => {
        handleInput(input);
        internal_eventEmitter.current.emit('input', input);
    }, [handleInput]);
    const schedulePendingInputFlush = useCallback(() => {
        clearPendingInputFlush();
        pendingInputFlushRef.current = setImmediate(() => {
            pendingInputFlushRef.current = undefined;
            const pendingEscape = inputParserRef.current.flushPendingEscape();
            if (!pendingEscape) {
                return;
            }
            emitInput(pendingEscape);
        });
    }, [clearPendingInputFlush, emitInput]);
    const handleReadable = useCallback(() => {
        clearPendingInputFlush();
        let chunk;
        // eslint-disable-next-line @typescript-eslint/ban-types
        while ((chunk = stdin.read()) !== null) {
            const inputEvents = inputParserRef.current.push(chunk);
            for (const input of inputEvents) {
                emitInput(input);
            }
        }
        if (inputParserRef.current.hasPendingEscape()) {
            schedulePendingInputFlush();
        }
    }, [stdin, emitInput, clearPendingInputFlush, schedulePendingInputFlush]);
    const handleSetRawMode = useCallback((isEnabled) => {
        if (!isRawModeSupported) {
            if (stdin === process.stdin) {
                throw new Error('Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported');
            }
            else {
                throw new Error('Raw mode is not supported on the stdin provided to Ink.\nRead about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported');
            }
        }
        stdin.setEncoding('utf8');
        if (isEnabled) {
            // Ensure raw mode is enabled only once
            if (rawModeEnabledCount.current === 0) {
                stdin.ref();
                stdin.setRawMode(true);
                // Store the listener reference to avoid stale closure when removing
                readableListenerRef.current = handleReadable;
                stdin.addListener('readable', handleReadable);
            }
            rawModeEnabledCount.current++;
            return;
        }
        // Disable raw mode only when no components left that are using it
        if (rawModeEnabledCount.current === 0) {
            return;
        }
        if (--rawModeEnabledCount.current === 0) {
            disableRawMode();
        }
    }, [isRawModeSupported, stdin, handleReadable, disableRawMode]);
    // Focus navigation helpers
    const findNextFocusable = useCallback((currentFocusables, currentActiveFocusId) => {
        const activeIndex = currentFocusables.findIndex(focusable => {
            return focusable.id === currentActiveFocusId;
        });
        for (let index = activeIndex + 1; index < currentFocusables.length; index++) {
            const focusable = currentFocusables[index];
            if (focusable?.isActive) {
                return focusable.id;
            }
        }
        return undefined;
    }, []);
    const findPreviousFocusable = useCallback((currentFocusables, currentActiveFocusId) => {
        const activeIndex = currentFocusables.findIndex(focusable => {
            return focusable.id === currentActiveFocusId;
        });
        for (let index = activeIndex - 1; index >= 0; index--) {
            const focusable = currentFocusables[index];
            if (focusable?.isActive) {
                return focusable.id;
            }
        }
        return undefined;
    }, []);
    const focusNext = useCallback(() => {
        setFocusables(currentFocusables => {
            setActiveFocusId(currentActiveFocusId => {
                const firstFocusableId = currentFocusables.find(focusable => focusable.isActive)?.id;
                const nextFocusableId = findNextFocusable(currentFocusables, currentActiveFocusId);
                return nextFocusableId ?? firstFocusableId;
            });
            return currentFocusables;
        });
    }, [findNextFocusable]);
    const focusPrevious = useCallback(() => {
        setFocusables(currentFocusables => {
            setActiveFocusId(currentActiveFocusId => {
                const lastFocusableId = currentFocusables.findLast(focusable => focusable.isActive)?.id;
                const previousFocusableId = findPreviousFocusable(currentFocusables, currentActiveFocusId);
                return previousFocusableId ?? lastFocusableId;
            });
            return currentFocusables;
        });
    }, [findPreviousFocusable]);
    // Handle tab navigation via effect that subscribes to input events
    useEffect(() => {
        const handleTabNavigation = (input) => {
            if (!isFocusEnabled || focusablesCountRef.current === 0)
                return;
            if (input === tab) {
                focusNext();
            }
            if (input === shiftTab) {
                focusPrevious();
            }
        };
        internal_eventEmitter.current.on('input', handleTabNavigation);
        const emitter = internal_eventEmitter.current;
        return () => {
            emitter.off('input', handleTabNavigation);
        };
    }, [isFocusEnabled, focusNext, focusPrevious]);
    const enableFocus = useCallback(() => {
        setIsFocusEnabled(true);
    }, []);
    const disableFocus = useCallback(() => {
        setIsFocusEnabled(false);
    }, []);
    const focus = useCallback((id) => {
        setFocusables(currentFocusables => {
            const hasFocusableId = currentFocusables.some(focusable => focusable?.id === id);
            if (hasFocusableId) {
                setActiveFocusId(id);
            }
            return currentFocusables;
        });
    }, []);
    const addFocusable = useCallback((id, { autoFocus }) => {
        setFocusables(currentFocusables => {
            focusablesCountRef.current = currentFocusables.length + 1;
            return [
                ...currentFocusables,
                {
                    id,
                    isActive: true,
                },
            ];
        });
        if (autoFocus) {
            setActiveFocusId(currentActiveFocusId => {
                if (!currentActiveFocusId) {
                    return id;
                }
                return currentActiveFocusId;
            });
        }
    }, []);
    const removeFocusable = useCallback((id) => {
        setActiveFocusId(currentActiveFocusId => {
            if (currentActiveFocusId === id) {
                return undefined;
            }
            return currentActiveFocusId;
        });
        setFocusables(currentFocusables => {
            const filtered = currentFocusables.filter(focusable => {
                return focusable.id !== id;
            });
            focusablesCountRef.current = filtered.length;
            return filtered;
        });
    }, []);
    const activateFocusable = useCallback((id) => {
        setFocusables(currentFocusables => currentFocusables.map(focusable => {
            if (focusable.id !== id) {
                return focusable;
            }
            return {
                id,
                isActive: true,
            };
        }));
    }, []);
    const deactivateFocusable = useCallback((id) => {
        setActiveFocusId(currentActiveFocusId => {
            if (currentActiveFocusId === id) {
                return undefined;
            }
            return currentActiveFocusId;
        });
        setFocusables(currentFocusables => currentFocusables.map(focusable => {
            if (focusable.id !== id) {
                return focusable;
            }
            return {
                id,
                isActive: false,
            };
        }));
    }, []);
    // Handle cursor visibility and raw mode cleanup on unmount
    useEffect(() => {
        return () => {
            cliCursor.show(stdout);
            if (isRawModeSupported && rawModeEnabledCount.current > 0) {
                disableRawMode();
            }
        };
    }, [stdout, isRawModeSupported, disableRawMode]);
    // Memoize context values to prevent unnecessary re-renders
    const appContextValue = useMemo(() => ({
        exit: handleExit,
    }), [handleExit]);
    const stdinContextValue = useMemo(() => ({
        stdin,
        setRawMode: handleSetRawMode,
        isRawModeSupported,
        // eslint-disable-next-line @typescript-eslint/naming-convention
        internal_exitOnCtrlC: exitOnCtrlC,
        // eslint-disable-next-line @typescript-eslint/naming-convention
        internal_eventEmitter: internal_eventEmitter.current,
    }), [stdin, handleSetRawMode, isRawModeSupported, exitOnCtrlC]);
    const stdoutContextValue = useMemo(() => ({
        stdout,
        write: writeToStdout,
    }), [stdout, writeToStdout]);
    const stderrContextValue = useMemo(() => ({
        stderr,
        write: writeToStderr,
    }), [stderr, writeToStderr]);
    const cursorContextValue = useMemo(() => ({
        setCursorPosition,
    }), [setCursorPosition]);
    const focusContextValue = useMemo(() => ({
        activeId: activeFocusId,
        add: addFocusable,
        remove: removeFocusable,
        activate: activateFocusable,
        deactivate: deactivateFocusable,
        enableFocus,
        disableFocus,
        focusNext,
        focusPrevious,
        focus,
    }), [
        activeFocusId,
        addFocusable,
        removeFocusable,
        activateFocusable,
        deactivateFocusable,
        enableFocus,
        disableFocus,
        focusNext,
        focusPrevious,
        focus,
    ]);
    return (React.createElement(AppContext.Provider, { value: appContextValue },
        React.createElement(StdinContext.Provider, { value: stdinContextValue },
            React.createElement(StdoutContext.Provider, { value: stdoutContextValue },
                React.createElement(StderrContext.Provider, { value: stderrContextValue },
                    React.createElement(FocusContext.Provider, { value: focusContextValue },
                        React.createElement(CursorContext.Provider, { value: cursorContextValue },
                            React.createElement(ErrorBoundary, { onError: handleExit }, children))))))));
}
App.displayName = 'InternalApp';
export default App;
//# sourceMappingURL=App.js.map