// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import clsx from 'clsx';
import { useMergeRefs, useResizeObserver, useUniqueId } from '@cloudscape-design/component-toolkit/internal';
import { getLogicalBoundingClientRect } from '@cloudscape-design/component-toolkit/internal';
import { getBaseProps } from '../internal/base-component';
import { getFirstFocusable, getLastFocusable } from '../internal/components/focus-lock/utils.js';
import TabTrap from '../internal/components/tab-trap/index.js';
import { Transition } from '../internal/components/transition';
import { fireNonCancelableEvent } from '../internal/events';
import customCssProps from '../internal/generated/custom-css-properties';
import { useMobile } from '../internal/hooks/use-mobile';
import { usePortalModeClasses } from '../internal/hooks/use-portal-mode-classes';
import { useVisualRefresh } from '../internal/hooks/use-visual-mode';
import { nodeBelongs } from '../internal/utils/node-belongs';
import { DropdownContextProvider } from './context';
import { calculatePosition, hasEnoughSpaceForFlexibleWidth, } from './dropdown-fit-handler';
import { applyDropdownPositionRelativeToViewport } from './dropdown-position';
import { getDropdownStyles } from './style';
import styles from './styles.css.js';
import testUtilStyles from './test-classes/styles.css.js';
const DropdownContainer = ({ triggerRef, children, renderWithPortal, id, referrerId, open, }) => {
    var _a, _b;
    if (!renderWithPortal) {
        return React.createElement(React.Fragment, null, children);
    }
    if (!open) {
        return null;
    }
    const currentDocument = (_b = (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.ownerDocument) !== null && _b !== void 0 ? _b : document;
    return createPortal(React.createElement("div", { id: id, "data-awsui-referrer-id": referrerId }, children), currentDocument.body);
};
const TransitionContent = ({ state, transitionRef, dropdownClasses, matchTriggerWidth, hideBlockBorder, interior, isRefresh, dropdownRef, verticalContainerRef, expandToViewport, minWidth, maxWidth, header, content, footer, position, open, onMouseDown, onFocusEnter, onFocusLeave, id, ariaRole, ariaLabel, ariaLabelledby, ariaDescribedby, dropdownStyle, }) => {
    const contentRef = useMergeRefs(dropdownRef, transitionRef);
    const dropdownStyles = {};
    if (minWidth) {
        dropdownStyles[customCssProps.dropdownDefaultMinWidth] = minWidth;
    }
    if (maxWidth) {
        dropdownStyles[customCssProps.dropdownDefaultMaxWidth] = maxWidth;
    }
    return (React.createElement("div", { className: clsx(styles.dropdown, dropdownClasses, {
            [styles.open]: open,
            [styles['with-limited-width']]: !matchTriggerWidth,
            [styles['hide-block-border']]: hideBlockBorder,
            [styles.interior]: interior,
            [styles.refresh]: isRefresh,
            [styles['use-portal']]: expandToViewport && !interior,
            [styles['use-flexible-width']]: !matchTriggerWidth && !interior,
        }), ref: contentRef, id: id, role: ariaRole, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "data-open": open, "data-animating": state !== 'exited', "aria-hidden": !open, style: dropdownStyles, onMouseDown: onMouseDown, onFocus: onFocusEnter, onBlur: onFocusLeave },
        React.createElement("div", { className: clsx(styles['dropdown-content-wrapper'], !header && !content && styles['is-empty'], isRefresh && styles.refresh), style: dropdownStyle },
            React.createElement("div", { ref: verticalContainerRef, className: styles['dropdown-content'] },
                React.createElement(DropdownContextProvider, { position: position },
                    header && React.createElement("div", { className: testUtilStyles.header }, header),
                    content,
                    footer && React.createElement("div", { className: testUtilStyles.footer }, footer))))));
};
const InternalDropdown = ({ content, trigger, triggerRef: externalTriggerRef, triggerId: externalTriggerId, open, onOutsideClick, onMouseDown, header, footer, dropdownId, stretchTriggerHeight = false, stretchHeight = false, minWidth, maxWidth, maxHeight, hideBlockBorder = true, expandToViewport = false, preferredAlignment = 'start', interior = false, scrollable = true, loopFocus = expandToViewport, onFocus, onBlur, onFocusEnter, onFocusLeave, onEscape, contentKey, dropdownContentId, ariaRole, ariaLabel, ariaLabelledby, ariaDescribedby, style, __internalRootRef, ...restProps }) => {
    const baseProps = getBaseProps(restProps);
    const wrapperRef = useRef(null);
    const mergedRef = useMergeRefs(wrapperRef, __internalRootRef);
    const internalTriggerRef = useRef(null);
    const dropdownRef = useRef(null);
    const dropdownContainerRef = useRef(null);
    const verticalContainerRef = useRef(null);
    // To keep track of the initial position (drop up/down) which is kept the same during fixed repositioning
    const fixedPosition = useRef(null);
    // Use external trigger ref if provided, otherwise use internal ref
    const triggerRef = externalTriggerRef || internalTriggerRef;
    const isRefresh = useVisualRefresh();
    const dropdownClasses = usePortalModeClasses(triggerRef);
    const [position, setPosition] = useState('bottom-right');
    const isMobile = useMobile();
    // Derive if dropdown should match trigger width exactly
    // This happens when both minWidth and maxWidth are explicitly set to 'trigger'
    const matchTriggerWidth = minWidth === 'trigger' && maxWidth === 'trigger';
    // Convert preferredAlignment to boolean for internal positioning logic
    const preferCenter = preferredAlignment === 'center';
    const setDropdownPosition = (position, triggerBox, target, verticalContainer) => {
        verticalContainer.style.maxBlockSize = position.blockSize;
        // Only apply occupy-entire-width when matching trigger width exactly and not in portal mode
        if (!interior && matchTriggerWidth && !expandToViewport) {
            target.classList.add(styles['occupy-entire-width']);
        }
        else {
            target.style.inlineSize = position.inlineSize;
        }
        // Using styles for main dropdown to adjust its position as preferred alternative
        if (position.dropBlockStart && !interior) {
            target.classList.add(styles['dropdown-drop-up']);
            if (!expandToViewport) {
                target.style.insetBlockEnd = '100%';
            }
        }
        else {
            target.classList.remove(styles['dropdown-drop-up']);
        }
        target.classList.add(position.dropInlineStart ? styles['dropdown-drop-left'] : styles['dropdown-drop-right']);
        if (position.insetInlineStart && position.insetInlineStart !== 'auto') {
            target.style.insetInlineStart = position.insetInlineStart;
        }
        // Position normal overflow dropdowns with fixed positioning relative to viewport
        if (expandToViewport && !interior) {
            applyDropdownPositionRelativeToViewport({
                position,
                dropdownElement: target,
                triggerRect: triggerBox,
                isMobile,
            });
            // Keep track of the initial dropdown position and direction.
            // Dropdown direction doesn't need to change as the user scrolls, just needs to stay attached to the trigger.
            fixedPosition.current = position;
            return;
        }
        // For an interior dropdown (the fly out) we need exact values for positioning
        // and classes are not enough
        // usage of relative position is impossible due to overwrite of overflow-x
        if (interior && isInteriorPosition(position)) {
            if (position.dropBlockStart) {
                target.style.insetBlockEnd = position.insetBlockEnd;
            }
            else {
                target.style.insetBlockStart = position.insetBlockStart;
            }
            target.style.insetInlineStart = position.insetInlineStart;
        }
        if (position.dropBlockStart && position.dropInlineStart) {
            setPosition('top-left');
        }
        else if (position.dropBlockStart) {
            setPosition('top-right');
        }
        else if (position.dropInlineStart) {
            setPosition('bottom-left');
        }
        else {
            setPosition('bottom-right');
        }
    };
    const isOutsideDropdown = (element) => (!wrapperRef.current || !nodeBelongs(wrapperRef.current, element)) &&
        (!dropdownContainerRef.current || !nodeBelongs(dropdownContainerRef.current, element));
    const focusHandler = (event) => {
        if (!event.relatedTarget || isOutsideDropdown(event.relatedTarget)) {
            fireNonCancelableEvent(onFocus, event);
        }
    };
    const blurHandler = (event) => {
        if (!event.relatedTarget || isOutsideDropdown(event.relatedTarget)) {
            fireNonCancelableEvent(onBlur, event);
        }
    };
    const isOutsideDropdownContent = (element) => !dropdownRef.current || !nodeBelongs(dropdownRef.current, element);
    const focusEnterHandler = (event) => {
        if (!event.relatedTarget || isOutsideDropdownContent(event.relatedTarget)) {
            fireNonCancelableEvent(onFocusEnter, event);
        }
    };
    const focusLeaveHandler = (event) => {
        if (!event.relatedTarget || isOutsideDropdownContent(event.relatedTarget)) {
            fireNonCancelableEvent(onFocusLeave, event);
        }
    };
    // Check if the dropdown has enough space to fit with its desired width constraints
    // If not, remove the class that allows flexible width sizing
    const fixStretching = () => {
        const classNameToRemove = styles['use-flexible-width'];
        if (open &&
            dropdownRef.current &&
            triggerRef.current &&
            dropdownRef.current.classList.contains(classNameToRemove) &&
            !hasEnoughSpaceForFlexibleWidth({
                triggerElement: triggerRef.current,
                dropdownElement: dropdownRef.current,
                minWidthConstraint: minWidth,
                maxWidthConstraint: maxWidth,
                expandToViewport,
                stretchHeight,
                isMobile,
            })) {
            dropdownRef.current.classList.remove(classNameToRemove);
        }
    };
    useResizeObserver(() => dropdownRef.current, fixStretching);
    useLayoutEffect(() => {
        const onDropdownOpen = () => {
            if (open && dropdownRef.current && triggerRef.current && verticalContainerRef.current) {
                if (scrollable) {
                    dropdownRef.current.classList.add(styles.nowrap);
                }
                setDropdownPosition(...calculatePosition(dropdownRef.current, triggerRef.current, verticalContainerRef.current, interior, expandToViewport, preferCenter, matchTriggerWidth, stretchHeight, isMobile, minWidth, maxWidth, maxHeight), dropdownRef.current, verticalContainerRef.current);
                if (scrollable) {
                    dropdownRef.current.classList.remove(styles.nowrap);
                }
            }
        };
        onDropdownOpen();
        if (open) {
            // window may scroll when dropdown opens, for example when soft keyboard shows up
            window.addEventListener('scroll', onDropdownOpen);
            // only listen to window scroll within very short time after the dropdown opens
            // do not want to interfere dropdown position on scroll afterwards
            const timeoutId = setTimeout(() => {
                window.removeEventListener('scroll', onDropdownOpen);
            }, 500);
            return () => {
                clearTimeout(timeoutId);
                window.removeEventListener('scroll', onDropdownOpen);
            };
        }
        // See AWSUI-13040
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [open, dropdownRef, triggerRef, verticalContainerRef, interior, matchTriggerWidth, isMobile, contentKey]);
    // subscribe to outside click
    useEffect(() => {
        if (!open) {
            return;
        }
        const clickListener = (event) => {
            // Since the listener is registered on the window, `event.target` will incorrectly point at the
            // shadow root if the component is rendered inside shadow DOM.
            const target = event.composedPath ? event.composedPath()[0] : event.target;
            // For internal triggers, the wrapper div itself counts as outside — only its children are the trigger.
            // For external triggers, the ref is the trigger element directly, so it counts as inside.
            const isOutsideTrigger = !nodeBelongs(triggerRef.current, target) || (!externalTriggerRef && target === triggerRef.current);
            if (!nodeBelongs(dropdownRef.current, target) && isOutsideTrigger) {
                fireNonCancelableEvent(onOutsideClick);
            }
        };
        window.addEventListener('click', clickListener, true);
        return () => {
            window.removeEventListener('click', clickListener, true);
        };
    }, [open, onOutsideClick, triggerRef, externalTriggerRef]);
    // subscribe to Escape key press
    useEffect(() => {
        // Only add the listener if onEscape callback is provided
        if (!open || !onEscape) {
            return;
        }
        const keydownListener = (event) => {
            if (event.key === 'Escape') {
                // Prevent any surrounding modals or dialogs from acting on this Escape key
                event.stopPropagation();
                fireNonCancelableEvent(onEscape);
            }
        };
        window.addEventListener('keydown', keydownListener, true);
        return () => {
            window.removeEventListener('keydown', keydownListener, true);
        };
    }, [open, onEscape]);
    // sync dropdown position on scroll and resize
    useLayoutEffect(() => {
        if (!expandToViewport || !open) {
            return;
        }
        const updateDropdownPosition = () => {
            if (triggerRef.current && dropdownRef.current && verticalContainerRef.current && fixedPosition.current) {
                applyDropdownPositionRelativeToViewport({
                    position: fixedPosition.current,
                    dropdownElement: dropdownRef.current,
                    triggerRect: getLogicalBoundingClientRect(triggerRef.current),
                    isMobile,
                });
            }
        };
        updateDropdownPosition();
        const controller = new AbortController();
        window.addEventListener('scroll', updateDropdownPosition, { capture: true, signal: controller.signal });
        window.addEventListener('resize', updateDropdownPosition, { capture: true, signal: controller.signal });
        return () => {
            controller.abort();
        };
    }, [open, expandToViewport, isMobile, triggerRef]);
    const generatedReferrerId = useUniqueId();
    const referrerId = externalTriggerId || generatedReferrerId;
    // Compute CSS variable values for min/max width
    // These will be used by the use-flexible-width CSS class
    const getMinWidthCssValue = () => {
        if (minWidth === undefined) {
            return undefined;
        }
        if (typeof minWidth === 'number') {
            return `${minWidth}px`;
        }
        // 'trigger' maps to 100% (relative to parent)
        return '100%';
    };
    const getMaxWidthCssValue = () => {
        if (maxWidth === undefined) {
            return 'none';
        }
        if (typeof maxWidth === 'number') {
            return `${maxWidth}px`;
        }
        // 'trigger' maps to 100% (relative to parent)
        return '100%';
    };
    return (React.createElement("div", { ...baseProps, className: clsx(styles.root, interior && styles.interior, stretchTriggerHeight && styles['stretch-trigger-height'], baseProps.className), ref: mergedRef, onFocus: focusHandler, onBlur: blurHandler },
        !externalTriggerRef && (React.createElement("div", { id: referrerId, className: clsx(stretchTriggerHeight && styles['stretch-trigger-height'], testUtilStyles.trigger), ref: internalTriggerRef }, trigger)),
        React.createElement(TabTrap, { focusNextCallback: () => { var _a; return dropdownRef.current && ((_a = getFirstFocusable(dropdownRef.current)) === null || _a === void 0 ? void 0 : _a.focus()); }, disabled: !open || !loopFocus }),
        React.createElement(DropdownContainer, { triggerRef: triggerRef, renderWithPortal: expandToViewport && !interior, id: dropdownId, referrerId: referrerId, open: open },
            React.createElement(Transition, { in: open !== null && open !== void 0 ? open : false, exit: false }, (state, ref) => (React.createElement("div", { ref: dropdownContainerRef },
                React.createElement(TabTrap, { focusNextCallback: () => { var _a; return triggerRef.current && ((_a = getLastFocusable(triggerRef.current)) === null || _a === void 0 ? void 0 : _a.focus()); }, disabled: !open || !loopFocus }),
                React.createElement(TransitionContent, { state: state, transitionRef: ref, dropdownClasses: dropdownClasses, open: open, matchTriggerWidth: matchTriggerWidth, hideBlockBorder: hideBlockBorder, interior: interior, header: header, content: content, expandToViewport: expandToViewport, minWidth: getMinWidthCssValue(), maxWidth: getMaxWidthCssValue(), footer: footer, onMouseDown: onMouseDown, onFocusEnter: focusEnterHandler, onFocusLeave: focusLeaveHandler, isRefresh: isRefresh, dropdownRef: dropdownRef, verticalContainerRef: verticalContainerRef, position: position, id: dropdownContentId, ariaRole: ariaRole, ariaLabel: ariaLabel, ariaLabelledby: ariaLabelledby, ariaDescribedby: ariaDescribedby, dropdownStyle: getDropdownStyles(style) }),
                React.createElement(TabTrap, { focusNextCallback: () => { var _a; return triggerRef.current && ((_a = getFirstFocusable(triggerRef.current)) === null || _a === void 0 ? void 0 : _a.focus()); }, disabled: !open || !loopFocus })))))));
};
const isInteriorPosition = (position) => position.insetBlockEnd !== undefined;
export default InternalDropdown;
//# sourceMappingURL=internal.js.map