// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import clsx from 'clsx';
import { useStableCallback, useUniqueId } from '@cloudscape-design/component-toolkit/internal';
import { useDropdownStatus } from '../../internal/components/dropdown-status';
import { fireKeyboardEvent, fireNonCancelableEvent } from '../../internal/events';
import { useIMEComposition } from '../../internal/hooks/use-ime-composition';
import { getFirstScrollableParent } from '../../internal/utils/scrollable-containers';
import Token from '../../token/internal';
import { calculateTokenPosition, CaretController, getOwnerSelection } from '../core/caret-controller';
import { extractTextFromCaretSpots } from '../core/caret-spot-utils';
import { findContainingReference, getMouseDown, isNonTypeablePosition, normalizeCollapsedCaret, normalizeSelection, setMouseDown, } from '../core/caret-utils';
import { NEXT_TICK_TIMEOUT } from '../core/constants';
import { createParagraph, findAllParagraphs } from '../core/dom-utils';
import { handleClipboardEvent, handleEditableKeyDown } from '../core/event-handlers';
import { handleMenuSelection, useMenuItems, useMenuLoadMore, } from '../core/menu-state';
import { extractTokensFromDOM, getPromptText, processTokens } from '../core/token-operations';
import { renderTokensToDOM } from '../core/token-renderer';
import { enforcePinnedTokenOrdering, getCaretPositionAfterPinnedReorder, getCaretPositionAfterTokenRemoval, mergeConsecutiveTextTokens, } from '../core/token-utils';
import { detectTriggerTransition } from '../core/trigger-utils';
import { isReferenceToken, isTextNode, isTextToken, isTriggerToken } from '../core/type-guards';
import { insertTextIntoContentEditable } from '../utils/insert-text-content-editable';
import { useShortcutsEffects, useShortcutsState } from './use-shortcuts';
import styles from '../styles.css.js';
import testutilStyles from '../test-classes/styles.css.js';
export function createEditableState() {
    return {
        skipNextZeroWidthUpdate: false,
        menuSelectionTokenId: null,
    };
}
/**
 * Determines if the token array changed structurally and needs a full DOM re-render.
 *
 * Compares token types, IDs, and text values. During normal typing the extracted
 * tokens already match the DOM, so text comparisons won't trigger unnecessary
 * re-renders. External updates (parent replacing tokens) will be detected by
 * the value difference and trigger a re-render.
 *
 * Returns 'none' if no changes, 'text-only' if only token values differ
 * (same count, types, and IDs — covers both text and trigger filter changes),
 * or 'structural' for any other change.
 */
function classifyChange(oldTokens, newTokens) {
    if (!oldTokens || !newTokens) {
        return 'structural';
    }
    if (oldTokens.length !== newTokens.length) {
        return 'structural';
    }
    let hasTextDiff = false;
    for (let i = 0; i < oldTokens.length; i++) {
        const oldToken = oldTokens[i];
        const newToken = newTokens[i];
        if (oldToken.type !== newToken.type) {
            return 'structural';
        }
        if (isReferenceToken(oldToken) && isReferenceToken(newToken)) {
            if (oldToken.id !== newToken.id) {
                return 'structural';
            }
        }
        if (isTriggerToken(oldToken) && isTriggerToken(newToken)) {
            if (oldToken.id !== newToken.id) {
                return 'structural';
            }
            // Transition between empty and non-empty filter text changes the underline styling.
            const wasEmpty = oldToken.value.length === 0;
            const isEmpty = newToken.value.length === 0;
            if (wasEmpty !== isEmpty) {
                return 'structural';
            }
            if (oldToken.value !== newToken.value) {
                hasTextDiff = true;
            }
        }
        if (isTextToken(oldToken) && isTextToken(newToken)) {
            if (oldToken.value !== newToken.value) {
                hasTextDiff = true;
            }
        }
    }
    return hasTextDiff ? 'text-only' : 'none';
}
/**
 * Positions the caret after a menu-selected reference token.
 * Returns true if the caret was positioned (token found), false otherwise.
 */
function positionCaretAfterMenuSelection(tokens, editableState, caretController) {
    if (!editableState.menuSelectionTokenId || !caretController) {
        return false;
    }
    const insertedTokenIndex = tokens.findIndex(t => isReferenceToken(t) && t.id === editableState.menuSelectionTokenId);
    if (insertedTokenIndex === -1) {
        return false;
    }
    const caretPos = calculateTokenPosition(tokens, insertedTokenIndex);
    caretController.setPosition(caretPos);
    editableState.menuSelectionTokenId = null;
    return true;
}
/** Finds a trigger token by its ID in the token array. */
function findTriggerTokenById(tokens, triggerId) {
    const trigger = tokens.find(token => isTriggerToken(token) && token.id === triggerId);
    if (trigger && isTriggerToken(trigger)) {
        return trigger;
    }
    return null;
}
/**
 * Processes token changes from both user input and external (parent) updates.
 *
 * For user input: runs trigger detection on extracted DOM tokens and emits
 * the processed result via onChange.
 *
 * For external updates: detects if the parent provided new tokens that need
 * trigger detection or ID assignment, and emits corrected tokens if so.
 * Skips processing when the tokens reference matches what we last emitted
 * (for example, the parent is echoing back our own onChange).
 */
function useTokenProcessor(config) {
    const { tokens, menus, tokensToText, onChange, onTriggerDetected, state } = config;
    const emitTokenChange = useStableCallback((newTokens) => {
        const value = tokensToText ? tokensToText(newTokens) : getPromptText(newTokens);
        state.markTokensAsSent(newTokens);
        onChange({ value, tokens: newTokens });
    });
    const markCancelledTriggers = useStableCallback((cancelledIds) => {
        for (const id of cancelledIds) {
            state.cancelledTriggerIds.current.add(id);
            // Prevent opening the menu if the active trigger was cancelled
            if (state.caretInTrigger === id) {
                state.setCaretInTrigger(null);
            }
        }
    });
    /** Processes tokens extracted from the DOM after user input. */
    const processUserInput = useStableCallback((inputTokens) => {
        const { tokens: processed, cancelledIds } = processTokens(inputTokens, { menus, tokensToText }, {
            source: 'user-input',
            detectTriggers: true,
        }, onTriggerDetected, state.cancelledTriggerIds.current);
        markCancelledTriggers(cancelledIds);
        emitTokenChange(processed);
    });
    /** Processes external token updates from the parent component. */
    useEffect(() => {
        // Skip if these are the tokens we just emitted — the parent is echoing back our onChange
        if (state.lastEmittedTokensRef.current === tokens) {
            return;
        }
        if (!tokens) {
            return;
        }
        const { tokens: processed, cancelledIds } = processTokens(tokens, { menus, tokensToText }, {
            source: 'external',
            detectTriggers: true,
        }, undefined, state.cancelledTriggerIds.current);
        markCancelledTriggers(cancelledIds);
        // Only emit if processing actually changed the tokens (e.g., new triggers detected,
        // IDs assigned). Compare by type, value, and ID to avoid false positives from
        // reference inequality after processTokens creates new token objects.
        const hasChanges = processed.length !== tokens.length ||
            processed.some((t, i) => {
                const orig = tokens[i];
                if (t.type !== orig.type || t.value !== orig.value) {
                    return true;
                }
                if (isReferenceToken(t) && isReferenceToken(orig) && t.id !== orig.id) {
                    return true;
                }
                if (isTriggerToken(t) && isTriggerToken(orig) && t.id !== orig.id) {
                    return true;
                }
                return false;
            });
        if (hasChanges) {
            emitTokenChange(processed);
        }
        else {
            // Mark as seen even when no changes — prevents re-processing on next render
            state.lastEmittedTokensRef.current = tokens;
        }
    }, [tokens, menus, tokensToText, state, emitTokenChange, markCancelledTriggers]);
    return {
        processUserInput,
    };
}
/** Encapsulates all token-mode logic: trigger detection, menu state, keyboard handling, and rendering. */
export function useTokenMode(config) {
    var _a, _b, _c;
    const { editableElementRef, caretControllerRef, tokens, tokensToText, menus, disabled, readOnly, autoFocus, placeholder, invalid, warning, ariaLabel, ariaLabelledby, ariaDescribedby, ariaRequired, disableBrowserAutocorrect, spellcheck, onChange, onTriggerDetected, onAction, onBlur, onFocus, onKeyDown, onKeyUp, onMenuItemSelect, onMenuFilter, onMenuLoadItems, i18nStrings, adjustInputHeight, } = config;
    const shortcutsState = useShortcutsState();
    const { markTokensAsSent } = shortcutsState;
    const activeTriggerToken = useMemo(() => {
        var _a;
        if (!tokens || !caretControllerRef.current) {
            return null;
        }
        const activeTriggerID = ((_a = caretControllerRef.current.findActiveTrigger()) === null || _a === void 0 ? void 0 : _a.id) || null;
        if (!activeTriggerID) {
            return null;
        }
        if (shortcutsState.cancelledTriggerIds.current.has(activeTriggerID)) {
            return null;
        }
        return findTriggerTokenById(tokens, activeTriggerID);
        // eslint-disable-next-line react-hooks/exhaustive-deps -- caretInTrigger is the invalidation signal from useShortcutsEffects
    }, [tokens, caretControllerRef, shortcutsState.caretInTrigger]);
    const activeMenu = useMemo(() => {
        var _a;
        return activeTriggerToken && shortcutsState.caretInTrigger && !disabled && !readOnly
            ? ((_a = menus === null || menus === void 0 ? void 0 : menus.find(m => m.trigger === activeTriggerToken.triggerChar)) !== null && _a !== void 0 ? _a : null)
            : null;
    }, [activeTriggerToken, shortcutsState.caretInTrigger, menus, disabled, readOnly]);
    const menuIsOpen = !!activeMenu;
    const menuFilterText = (_a = activeTriggerToken === null || activeTriggerToken === void 0 ? void 0 : activeTriggerToken.value) !== null && _a !== void 0 ? _a : '';
    const processor = useTokenProcessor({
        tokens,
        menus,
        tokensToText,
        onChange,
        onTriggerDetected,
        state: shortcutsState,
    });
    const { processUserInput } = processor;
    useShortcutsEffects({
        tokens,
        editableElementRef,
        state: shortcutsState,
        activeTriggerToken,
        caretController: caretControllerRef,
    });
    const triggerWrapperRef = useRef(null);
    const [triggerVisible, setTriggerVisible] = useState(true);
    // Resolve the trigger DOM element synchronously during render from the tracked map.
    // The Dropdown's contentKey (which includes activeTriggerToken.id) handles
    // repositioning when the active trigger changes — no open/close toggle needed.
    if ((activeTriggerToken === null || activeTriggerToken === void 0 ? void 0 : activeTriggerToken.id) && menuIsOpen) {
        triggerWrapperRef.current = (_b = shortcutsState.triggerElementsRef.current.get(activeTriggerToken.id)) !== null && _b !== void 0 ? _b : null;
    }
    else {
        triggerWrapperRef.current = null;
    }
    const triggerWrapperReady = !!triggerWrapperRef.current && menuIsOpen;
    // Hide menu dropdown when trigger scrolls out of the editable container
    useEffect(() => {
        if (!menuIsOpen || !triggerWrapperRef.current || !editableElementRef.current) {
            setTriggerVisible(true);
            return;
        }
        const trigger = triggerWrapperRef.current;
        const container = editableElementRef.current;
        const checkTriggerVisibility = () => {
            const triggerRect = trigger.getBoundingClientRect();
            const containerRect = container.getBoundingClientRect();
            const isOutOfView = triggerRect.bottom < containerRect.top || triggerRect.top > containerRect.bottom;
            setTriggerVisible(!isOutOfView);
        };
        checkTriggerVisibility();
        container.addEventListener('scroll', checkTriggerVisibility);
        const scrollableParent = getFirstScrollableParent(container);
        if (scrollableParent) {
            scrollableParent.addEventListener('scroll', checkTriggerVisibility);
        }
        return () => {
            container.removeEventListener('scroll', checkTriggerVisibility);
            if (scrollableParent) {
                scrollableParent.removeEventListener('scroll', checkTriggerVisibility);
            }
        };
    }, [menuIsOpen, editableElementRef, activeTriggerToken]);
    const portalContainersRef = useRef(new Map());
    const [portalContainers, setPortalContainers] = useState([]);
    const renderTokens = useCallback((tokens, target) => {
        const result = renderTokensToDOM(tokens, target, portalContainersRef.current, shortcutsState.triggerElementsRef.current, shortcutsState.cancelledTriggerIds.current);
        // Update trigger element map from render result
        shortcutsState.triggerElementsRef.current = new Map(result.triggerElements);
        setPortalContainers(Array.from(portalContainersRef.current.values()));
        return result;
    }, [shortcutsState.triggerElementsRef, shortcutsState.cancelledTriggerIds, portalContainersRef]);
    useLayoutEffect(() => {
        if (editableElementRef.current && !caretControllerRef.current) {
            caretControllerRef.current = new CaretController(editableElementRef.current);
        }
    }, [editableElementRef, caretControllerRef]);
    const editableState = useRef(createEditableState()).current;
    const [tokenOperationAnnouncement, setTokenOperationAnnouncement] = useState('');
    const announceTokenOperation = useStableCallback((message) => {
        setTokenOperationAnnouncement(message);
    });
    const lastRenderedTokensRef = useRef(undefined);
    const lastDisabledRef = useRef(disabled);
    const lastReadOnlyRef = useRef(readOnly);
    // Refs to avoid recreating handleInput when these change (new array reference each render).
    const menusRef = useRef(menus);
    menusRef.current = menus;
    // Stable ref so the compositionend callback always calls the latest handleInput.
    const handleInputRef = useRef(() => { });
    // `composing` React state drives placeholder visibility — hidden while the user
    // is actively composing CJK characters so intermediate characters don't flash
    // the placeholder on and off.
    const [composing, setComposing] = useState(false);
    // Track IME composition state so the onInput handler skips intermediate characters
    // during CJK (and other) composition sequences. The onCompositionEnd callback
    // fires synchronously when the user commits a composed character — handleInput
    // runs unconditionally there. `isComposing()` stays true briefly after compositionend
    // (until the next animation frame) to block the spurious Enter keydown that some
    // browsers fire immediately after compositionend.
    const { isComposing } = useIMEComposition(editableElementRef, {
        onCompositionStart: () => setComposing(true),
        onCompositionEnd: () => {
            setComposing(false);
            handleInputRef.current();
        },
    });
    const handleInput = useCallback(() => {
        var _a, _b, _c, _d;
        if (!editableElementRef.current) {
            return;
        }
        const ownerDoc = (_a = editableElementRef.current.ownerDocument) !== null && _a !== void 0 ? _a : document;
        const cc = caretControllerRef.current;
        if (cc) {
            cc.capture();
        }
        if (editableState.skipNextZeroWidthUpdate) {
            editableState.skipNextZeroWidthUpdate = false;
        }
        const { movedTextNode } = extractTextFromCaretSpots(portalContainersRef.current, true);
        // Extract filter text from cancelled triggers, moving it to the paragraph level
        for (const id of shortcutsState.cancelledTriggerIds.current) {
            const trigger = (_b = shortcutsState.triggerElementsRef.current.get(id)) !== null && _b !== void 0 ? _b : editableElementRef.current.querySelector(`#${CSS.escape(id)}`);
            if (!trigger) {
                continue;
            }
            const filterText = (trigger.textContent || '').substring(1);
            if (!filterText) {
                continue;
            }
            let caretInTrigger = false;
            const selection = editableElementRef.current ? getOwnerSelection(editableElementRef.current) : null;
            if ((selection === null || selection === void 0 ? void 0 : selection.rangeCount) && trigger.contains(selection.getRangeAt(0).startContainer)) {
                caretInTrigger = true;
            }
            const textNode = ownerDoc.createTextNode(filterText);
            (_c = trigger.parentNode) === null || _c === void 0 ? void 0 : _c.insertBefore(textNode, trigger.nextSibling);
            trigger.textContent = (trigger.textContent || '').charAt(0);
            if (caretInTrigger && !movedTextNode) {
                if (cc) {
                    cc.positionAfterText(textNode);
                }
            }
        }
        if (movedTextNode && cc) {
            cc.positionAfterText(movedTextNode);
        }
        const directTextNodes = Array.from(editableElementRef.current.childNodes).filter(node => { var _a; return isTextNode(node) && ((_a = node.textContent) === null || _a === void 0 ? void 0 : _a.trim()); });
        if (directTextNodes.length > 0) {
            if (cc) {
                cc.capture();
            }
            let targetP = findAllParagraphs(editableElementRef.current)[0];
            if (!targetP) {
                targetP = createParagraph();
                editableElementRef.current.appendChild(targetP);
            }
            directTextNodes.forEach(textNode => {
                targetP.appendChild(textNode);
            });
            if (cc) {
                cc.restore();
            }
        }
        let extractedTokens = extractTokensFromDOM(editableElementRef.current, menusRef.current, portalContainersRef.current);
        // When a new trigger appears in the DOM, render immediately to ensure the
        // trigger element exists before subsequent caret operations.
        const newTriggers = extractedTokens.filter(isTriggerToken);
        const existingTriggerIds = new Set(shortcutsState.triggerElementsRef.current.keys());
        if (newTriggers.some(t => !existingTriggerIds.has(t.id))) {
            renderTokens(extractedTokens, editableElementRef.current);
        }
        const movedTokens = enforcePinnedTokenOrdering(extractedTokens);
        const tokensWereMoved = movedTokens.some((t, i) => t !== extractedTokens[i]);
        const mergedTokens = mergeConsecutiveTextTokens(movedTokens);
        if (tokensWereMoved) {
            const caretPosBeforeMove = (_d = cc === null || cc === void 0 ? void 0 : cc.getPosition()) !== null && _d !== void 0 ? _d : 0;
            const adjustedPosition = getCaretPositionAfterPinnedReorder(extractedTokens, mergedTokens, caretPosBeforeMove);
            extractedTokens = mergedTokens;
            renderTokens(mergedTokens, editableElementRef.current);
            if (editableElementRef.current && ownerDoc.activeElement === editableElementRef.current && cc) {
                cc.setPosition(adjustedPosition);
            }
        }
        processUserInput(extractedTokens);
        adjustInputHeight();
    }, [
        processUserInput,
        adjustInputHeight,
        editableElementRef,
        caretControllerRef,
        portalContainersRef,
        editableState,
        renderTokens,
        shortcutsState.triggerElementsRef,
        shortcutsState.cancelledTriggerIds,
    ]);
    // Keep the ref in sync so the compositionend handler always calls the latest version.
    handleInputRef.current = handleInput;
    // Token render effect
    useLayoutEffect(() => {
        if (!editableElementRef.current) {
            return;
        }
        const cc = caretControllerRef.current;
        const orderedTokens = tokens ? enforcePinnedTokenOrdering(tokens) : tokens;
        const prevOrderedTokens = lastRenderedTokensRef.current;
        const stateChanged = lastDisabledRef.current !== disabled || lastReadOnlyRef.current !== readOnly;
        lastDisabledRef.current = disabled;
        lastReadOnlyRef.current = readOnly;
        const triggerTransition = detectTriggerTransition(lastRenderedTokensRef.current, orderedTokens);
        const changeType = classifyChange(lastRenderedTokensRef.current, orderedTokens);
        const needsRerender = stateChanged || changeType !== 'none' || triggerTransition > 0;
        if (!needsRerender) {
            positionCaretAfterMenuSelection(orderedTokens !== null && orderedTokens !== void 0 ? orderedTokens : [], editableState, cc);
            lastRenderedTokensRef.current = orderedTokens;
            return;
        }
        // Text-only changes from user input are already reflected in the DOM by the
        // browser's native editing. Skip re-render to preserve cursor position —
        // Text-only changes from user input are already reflected in the DOM by the
        // browser's native editing. Skip re-render to preserve cursor position —
        // re-rendering would read a corrupted position from normalizeCaretIntoTrigger.
        // External updates still need a re-render.
        if (changeType === 'text-only' &&
            !stateChanged &&
            triggerTransition === 0 &&
            shortcutsState.lastEmittedTokensRef.current === tokens) {
            lastRenderedTokensRef.current = orderedTokens;
            return;
        }
        if (triggerTransition > 0 && orderedTokens && cc) {
            renderTokens(orderedTokens, editableElementRef.current);
            lastRenderedTokensRef.current = orderedTokens;
            cc.setPosition(triggerTransition);
            adjustInputHeight();
            return;
        }
        // A new trigger appeared (e.g. user typed a trigger char, possibly replacing a selection).
        // Position the caret after the trigger character so the user can start typing a filter.
        const newTrigger = orderedTokens &&
            prevOrderedTokens &&
            orderedTokens.find(t => isTriggerToken(t) && !prevOrderedTokens.some(p => isTriggerToken(p) && p.id === t.id));
        if (newTrigger && isTriggerToken(newTrigger) && cc) {
            renderTokens(orderedTokens, editableElementRef.current);
            lastRenderedTokensRef.current = orderedTokens;
            // Use the saved position from insertText if available, otherwise position after the trigger char
            const savedPos = cc.getSavedPosition();
            if (savedPos !== null) {
                cc.setPosition(savedPos);
            }
            else {
                const triggerIndex = orderedTokens.indexOf(newTrigger);
                const posBeforeTrigger = triggerIndex > 0 ? calculateTokenPosition(orderedTokens, triggerIndex - 1) : 0;
                cc.setPosition(posBeforeTrigger + newTrigger.triggerChar.length);
            }
            adjustInputHeight();
            return;
        }
        if (lastRenderedTokensRef.current &&
            orderedTokens &&
            lastRenderedTokensRef.current.length === 0 &&
            orderedTokens.length === 0) {
            lastRenderedTokensRef.current = orderedTokens;
            return;
        }
        if (editableState.menuSelectionTokenId && cc) {
            const insertedTokenIndex = (orderedTokens !== null && orderedTokens !== void 0 ? orderedTokens : []).findIndex(t => isReferenceToken(t) && t.id === editableState.menuSelectionTokenId);
            if (insertedTokenIndex !== -1) {
                renderTokens(orderedTokens !== null && orderedTokens !== void 0 ? orderedTokens : [], editableElementRef.current);
                positionCaretAfterMenuSelection(orderedTokens !== null && orderedTokens !== void 0 ? orderedTokens : [], editableState, cc);
                lastRenderedTokensRef.current = orderedTokens;
                adjustInputHeight();
                return;
            }
        }
        lastRenderedTokensRef.current = orderedTokens;
        if (cc) {
            cc.capture();
        }
        renderTokens(orderedTokens !== null && orderedTokens !== void 0 ? orderedTokens : [], editableElementRef.current);
        if (positionCaretAfterMenuSelection(orderedTokens !== null && orderedTokens !== void 0 ? orderedTokens : [], editableState, cc)) {
            adjustInputHeight();
            return;
        }
        if (cc) {
            const savedPosition = cc.getSavedPosition();
            const restoredPosition = getCaretPositionAfterTokenRemoval(savedPosition, prevOrderedTokens !== null && prevOrderedTokens !== void 0 ? prevOrderedTokens : [], orderedTokens !== null && orderedTokens !== void 0 ? orderedTokens : []);
            if (restoredPosition !== null) {
                cc.setPosition(restoredPosition);
            }
            else {
                cc.restore();
            }
        }
        adjustInputHeight();
    }, [
        disabled,
        readOnly,
        tokens,
        adjustInputHeight,
        caretControllerRef,
        editableElementRef,
        portalContainersRef,
        editableState,
        renderTokens,
        shortcutsState,
    ]);
    useEffect(() => {
        var _a, _b;
        const handleSelectionChange = () => {
            var _a;
            const sel = editableElementRef.current ? getOwnerSelection(editableElementRef.current) : null;
            if (!(sel === null || sel === void 0 ? void 0 : sel.rangeCount) || !((_a = editableElementRef.current) === null || _a === void 0 ? void 0 : _a.contains(sel.getRangeAt(0).startContainer))) {
                return;
            }
            // Skip caret normalization during mousedown — the user may be starting a
            // drag selection from a caret-spot position adjacent to a reference.
            if (!getMouseDown()) {
                normalizeCollapsedCaret(sel);
            }
            normalizeSelection(sel);
        };
        const handleMouseDown = () => {
            setMouseDown(true);
        };
        const handleMouseUp = () => {
            var _a;
            setMouseDown(false);
            const sel = editableElementRef.current ? getOwnerSelection(editableElementRef.current) : null;
            if (!(sel === null || sel === void 0 ? void 0 : sel.rangeCount) || !((_a = editableElementRef.current) === null || _a === void 0 ? void 0 : _a.contains(sel.getRangeAt(0).startContainer))) {
                return;
            }
            normalizeCollapsedCaret(sel);
            normalizeSelection(sel);
            // Deferred re-check: browsers may finalize caret position after mouseup
            /* istanbul ignore next -- browser mouseup normalization covered by integ tests */
            requestAnimationFrame(() => {
                const sel = editableElementRef.current ? getOwnerSelection(editableElementRef.current) : null;
                if (!(sel === null || sel === void 0 ? void 0 : sel.rangeCount)) {
                    return;
                }
                const range = sel.getRangeAt(0);
                if (!range.collapsed) {
                    const startBad = isNonTypeablePosition(range.startContainer);
                    const endBad = isNonTypeablePosition(range.endContainer);
                    if (startBad) {
                        const ref = findContainingReference(range.startContainer);
                        if (ref === null || ref === void 0 ? void 0 : ref.parentNode) {
                            const index = Array.from(ref.parentNode.childNodes).indexOf(ref);
                            range.setStart(ref.parentNode, index);
                        }
                    }
                    if (endBad) {
                        const ref = findContainingReference(range.endContainer);
                        if (ref === null || ref === void 0 ? void 0 : ref.parentNode) {
                            const index = Array.from(ref.parentNode.childNodes).indexOf(ref);
                            range.setEnd(ref.parentNode, index + 1);
                        }
                    }
                }
                normalizeCollapsedCaret(sel);
                normalizeSelection(sel);
            });
        };
        const ownerDoc = (_b = (_a = editableElementRef.current) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : document;
        const element = editableElementRef.current;
        // selectionchange only fires on document — containment is checked inside the handler.
        ownerDoc.addEventListener('selectionchange', handleSelectionChange);
        // mousedown/mouseup scoped to the element to avoid reacting to clicks elsewhere.
        element === null || element === void 0 ? void 0 : element.addEventListener('mousedown', handleMouseDown);
        element === null || element === void 0 ? void 0 : element.addEventListener('mouseup', handleMouseUp);
        return () => {
            ownerDoc.removeEventListener('selectionchange', handleSelectionChange);
            element === null || element === void 0 ? void 0 : element.removeEventListener('mousedown', handleMouseDown);
            element === null || element === void 0 ? void 0 : element.removeEventListener('mouseup', handleMouseUp);
        };
        // eslint-disable-next-line react-hooks/exhaustive-deps -- only needs to run on mount
    }, []);
    const handleMenuSelect = useStableCallback((option) => {
        var _a, _b, _c, _d;
        if (!activeMenu || !activeTriggerToken || !tokens) {
            return;
        }
        shortcutsState.setCaretInTrigger(null);
        const result = handleMenuSelection(tokens, {
            value: option.option.value || '',
            label: option.option.label || option.option.value || '',
        }, activeMenu.id, (_a = activeMenu.useAtStart) !== null && _a !== void 0 ? _a : false, activeTriggerToken);
        const value = tokensToText ? tokensToText(result.tokens) : getPromptText(result.tokens);
        markTokensAsSent(result.tokens);
        editableState.menuSelectionTokenId = result.insertedToken.id || null;
        const isPinned = (_b = activeMenu.useAtStart) !== null && _b !== void 0 ? _b : false;
        const announcement = isPinned
            ? (_c = i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.tokenPinnedAriaLabel) === null || _c === void 0 ? void 0 : _c.call(i18nStrings, result.insertedToken)
            : (_d = i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.tokenInsertedAriaLabel) === null || _d === void 0 ? void 0 : _d.call(i18nStrings, result.insertedToken);
        if (announcement) {
            announceTokenOperation(announcement);
        }
        onChange({ value, tokens: result.tokens });
        fireNonCancelableEvent(onMenuItemSelect, {
            menuId: activeMenu.id,
            option: option.option,
        });
    });
    const [menuItemsState, menuItemsHandlers] = useMenuItems({
        menu: activeMenu !== null && activeMenu !== void 0 ? activeMenu : { id: '', trigger: '', options: [] },
        filterText: menuFilterText,
        onSelectItem: handleMenuSelect,
    });
    const menuStateRef = useRef({
        itemsState: menuItemsState,
        itemsHandlers: menuItemsHandlers,
        isOpen: menuIsOpen,
    });
    menuStateRef.current = {
        itemsState: menuItemsState,
        itemsHandlers: menuItemsHandlers,
        isOpen: menuIsOpen,
    };
    const closeMenu = useStableCallback(() => {
        shortcutsState.menuJustClosed.current = shortcutsState.caretInTrigger;
        shortcutsState.setCaretInTrigger(null);
    });
    const handleEditableElementKeyDown = useStableCallback((event) => {
        handleEditableKeyDown(event, {
            editableElement: editableElementRef.current,
            editableState,
            caretController: caretControllerRef.current,
            tokens,
            tokensToText,
            disabled,
            readOnly,
            i18nStrings,
            announceTokenOperation,
            getMenuOpen: () => menuStateRef.current.isOpen,
            getMenuItemsState: () => menuStateRef.current.itemsState,
            getMenuItemsHandlers: () => menuStateRef.current.itemsHandlers,
            getMenuStatusType: () => activeMenu === null || activeMenu === void 0 ? void 0 : activeMenu.statusType,
            closeMenu,
            onAction: onAction ? detail => fireNonCancelableEvent(onAction, detail) : undefined,
            onChange,
            markTokensAsSent,
            onKeyDown,
            isComposing,
        });
    });
    const handleEditableElementFocus = useStableCallback(() => {
        if (onFocus) {
            fireNonCancelableEvent(onFocus);
        }
    });
    const handleEditableElementBlur = useStableCallback(() => {
        if (onBlur) {
            fireNonCancelableEvent(onBlur);
        }
    });
    useEffect(() => {
        if (autoFocus && editableElementRef.current) {
            editableElementRef.current.focus();
        }
        // Intentionally run only on mount — autoFocus should fire once
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);
    useEffect(() => {
        var _a, _b, _c;
        const ownerWindow = (_c = ((_b = (_a = editableElementRef.current) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : document).defaultView) !== null && _c !== void 0 ? _c : window;
        const handleResize = () => adjustInputHeight();
        ownerWindow.addEventListener('resize', handleResize);
        return () => {
            ownerWindow.removeEventListener('resize', handleResize);
        };
    }, [adjustInputHeight, editableElementRef]);
    const menuLoadMoreResult = useMenuLoadMore({
        menu: activeMenu !== null && activeMenu !== void 0 ? activeMenu : { id: '', trigger: '', options: [] },
        statusType: (_c = activeMenu === null || activeMenu === void 0 ? void 0 : activeMenu.statusType) !== null && _c !== void 0 ? _c : 'finished',
        onLoadItems: detail => {
            fireNonCancelableEvent(onMenuLoadItems, detail);
        },
        onLoadMoreItems: () => {
            var _a;
            fireNonCancelableEvent(onMenuLoadItems, {
                menuId: (_a = activeMenu === null || activeMenu === void 0 ? void 0 : activeMenu.id) !== null && _a !== void 0 ? _a : '',
                filteringText: undefined,
                firstPage: false,
                samePage: false,
            });
        },
    });
    const menuLoadMoreHandlers = activeMenu ? menuLoadMoreResult : null;
    useEffect(() => {
        if (menuIsOpen && activeMenu && menuLoadMoreHandlers) {
            menuLoadMoreHandlers.fireLoadMoreOnMenuOpen();
        }
    }, [menuIsOpen, activeMenu, menuLoadMoreHandlers]);
    const prevMenuOpenRef = useRef(false);
    const prevItemsLengthRef = useRef(0);
    useEffect(() => {
        var _a;
        const justOpened = menuIsOpen && !prevMenuOpenRef.current;
        const itemsLoaded = menuIsOpen && prevMenuOpenRef.current && prevItemsLengthRef.current === 0 && menuItemsState.items.length > 0;
        if ((justOpened || itemsLoaded) && menuItemsHandlers && menuItemsState && menuItemsState.items.length > 0) {
            // Reset highlight so goHomeWithKeyboard triggers a state change even at index 0
            menuItemsHandlers.resetHighlightWithKeyboard();
            setTimeout(() => {
                menuItemsHandlers === null || menuItemsHandlers === void 0 ? void 0 : menuItemsHandlers.goHomeWithKeyboard();
            }, NEXT_TICK_TIMEOUT);
        }
        prevMenuOpenRef.current = menuIsOpen;
        prevItemsLengthRef.current = (_a = menuItemsState === null || menuItemsState === void 0 ? void 0 : menuItemsState.items.length) !== null && _a !== void 0 ? _a : 0;
        // eslint-disable-next-line react-hooks/exhaustive-deps -- only needs to react to menu open/close and item count changes
    }, [menuIsOpen, menuItemsState.items.length]);
    useEffect(() => {
        if (activeTriggerToken && activeMenu && onMenuFilter) {
            fireNonCancelableEvent(onMenuFilter, {
                menuId: activeMenu.id,
                filteringText: activeTriggerToken.value,
            });
        }
    }, [activeTriggerToken, activeMenu, onMenuFilter]);
    const handleLoadMore = useStableCallback(() => {
        if (menuLoadMoreHandlers) {
            menuLoadMoreHandlers.fireLoadMoreOnScroll();
        }
    });
    const menuListId = useUniqueId('menu-list');
    const menuFooterControlId = useUniqueId('menu-footer');
    const highlightedMenuOptionIdSource = useUniqueId();
    const highlightedMenuOptionId = (menuItemsState === null || menuItemsState === void 0 ? void 0 : menuItemsState.highlightedOption) ? highlightedMenuOptionIdSource : undefined;
    const menuDropdownStatusResult = useDropdownStatus({
        ...(activeMenu !== null && activeMenu !== void 0 ? activeMenu : {}),
        isEmpty: !menuItemsState || menuItemsState.items.length === 0,
        recoveryText: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.menuRecoveryText,
        errorIconAriaLabel: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.menuErrorIconAriaLabel,
        loadingText: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.menuLoadingText,
        finishedText: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.menuFinishedText,
        errorText: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.menuErrorText,
        onRecoveryClick: () => {
            var _a;
            if (menuLoadMoreHandlers) {
                menuLoadMoreHandlers.fireLoadMoreOnRecoveryClick();
            }
            (_a = editableElementRef.current) === null || _a === void 0 ? void 0 : _a.focus();
        },
        hasRecoveryCallback: Boolean(onMenuLoadItems),
    });
    const menuDropdownStatus = activeMenu ? menuDropdownStatusResult : null;
    const shouldRenderMenuDropdown = useMemo(() => {
        return !!(menuIsOpen && activeMenu && menuItemsState && triggerVisible);
    }, [menuIsOpen, activeMenu, menuItemsState, triggerVisible]);
    const showPlaceholder = !!(placeholder && (!tokens || tokens.length === 0) && !composing);
    const editableElementAttributes = {
        'aria-label': ariaLabel,
        'aria-labelledby': ariaLabelledby,
        'aria-describedby': ariaDescribedby,
        'aria-invalid': invalid ? 'true' : undefined,
        'aria-disabled': disabled ? 'true' : undefined,
        'aria-readonly': readOnly ? 'true' : undefined,
        'aria-required': ariaRequired ? 'true' : undefined,
        'data-placeholder': placeholder,
        className: clsx(styles.textarea, testutilStyles.textarea, {
            [styles.invalid]: invalid,
            [styles.warning]: warning,
            [styles['textarea-disabled']]: disabled,
            [styles['textarea-readonly']]: readOnly,
            [styles['placeholder-visible']]: showPlaceholder,
        }),
        autoCorrect: disableBrowserAutocorrect ? 'off' : undefined,
        autoCapitalize: disableBrowserAutocorrect ? 'off' : undefined,
        spellCheck: spellcheck,
        // When disabled, omit tabIndex entirely so the div is not focusable at all.
        tabIndex: disabled ? undefined : 0,
        onKeyDown: handleEditableElementKeyDown,
        onKeyUp: onKeyUp && (event => fireKeyboardEvent(onKeyUp, event)),
        onBlur: handleEditableElementBlur,
        onFocus: handleEditableElementFocus,
        onCopy: (event) => {
            if (editableElementRef.current) {
                handleClipboardEvent(event, editableElementRef.current, false);
            }
        },
        onCut: (event) => {
            if (editableElementRef.current) {
                handleClipboardEvent(event, editableElementRef.current, !disabled && !readOnly);
                if (!disabled && !readOnly) {
                    handleInput();
                }
            }
        },
        onPaste: (event) => {
            event.preventDefault();
            if (disabled || readOnly) {
                return;
            }
            const text = event.clipboardData.getData('text/plain');
            if (!text || !editableElementRef.current || !caretControllerRef.current) {
                return;
            }
            const cc = caretControllerRef.current;
            // Pass undefined for caretStart/caretEnd — let insertText calculate
            // the position after the selection is deleted by replaceSelection.
            insertTextIntoContentEditable(editableElementRef.current, text, undefined, undefined, cc, true);
        },
    };
    const portals = portalContainers.map(container => ReactDOM.createPortal(React.createElement(Token, { key: container.id, variant: "inline", label: container.label, disabled: !!disabled, readOnly: !!readOnly }), container.element));
    return {
        portalContainersRef,
        portalContainers,
        portals,
        editableState,
        editableElementAttributes,
        activeTriggerToken,
        activeMenu,
        menuIsOpen,
        menuFilterText,
        triggerWrapperRef,
        triggerWrapperReady,
        menuListId,
        menuFooterControlId,
        highlightedMenuOptionId,
        menuItemsState,
        menuItemsHandlers,
        menuDropdownStatus,
        shouldRenderMenuDropdown,
        handleInput: () => {
            // Skip processing during IME composition — intermediate input events fire
            // for each keystroke during CJK composition and must not trigger token
            // detection. The onCompositionEnd callback calls handleInput directly
            // (bypassing this guard) once the final text is committed.
            // Note: we use `composing` (React state) rather than `isComposing()` (ref)
            // so that input events after compositionend — such as the user immediately
            // deleting the committed text — are processed normally. The spurious-Enter
            // guard in the keyboard handler uses `isComposing()` separately.
            if (!composing) {
                handleInput();
            }
        },
        handleLoadMore,
        tokenOperationAnnouncement,
        markTokensAsSent,
    };
}
//# sourceMappingURL=use-token-mode.js.map