import { useState, useRef } from 'react';
import { mergeRefs } from '../../utils/mergeRefs';
import { noop, callAll, getElementHeight, getAutoHeightDuration, useUniqueId, useEffectAfterMount, useControlledState, } from './utils';
const easeInOut = 'cubic-bezier(0.4, 0, 0.2, 1)';
export default function useCollapse({ duration, easing = easeInOut, collapseStyles = {}, expandStyles = {}, onExpandStart = noop, onExpandEnd = noop, onCollapseStart = noop, onCollapseEnd = noop, isExpanded: configIsExpanded, defaultExpanded = false, hasDisabledAnimation = false, ...initialConfig } = {}) {
    const [isExpanded, setExpanded] = useControlledState(configIsExpanded, defaultExpanded);
    const uniqueId = useUniqueId();
    const el = useRef(null);
    const collapsedHeight = `${initialConfig.collapsedHeight || 0}px`;
    const collapsedStyles = {
        display: 'block',
        height: collapsedHeight,
        overflow: 'hidden',
    };
    const [exitAnimationEnded, setExitAnimationEnded] = useState(!configIsExpanded);
    const [styles, setStylesRaw] = useState(isExpanded ? {} : collapsedStyles);
    const setStyles = (newStyles) => {
        // We rely on reading information from layout
        // at arbitrary times, so ensure all style changes
        // happen before we might attempt to read them.
        setStylesRaw(newStyles);
    };
    const mergeStyles = (newStyles) => {
        setStyles(oldStyles => ({ ...oldStyles, ...newStyles }));
    };
    function getTransitionStyles(height) {
        if (hasDisabledAnimation) {
            return {};
        }
        const _duration = duration || getAutoHeightDuration(height);
        return {
            transition: `height ${_duration}ms ${easing}`,
        };
    }
    useEffectAfterMount(() => {
        let animationFrameTimer;
        let innerAnimationFrameTimer;
        if (isExpanded) {
            setExitAnimationEnded(false);
            animationFrameTimer = window.requestAnimationFrame(() => {
                onExpandStart();
                mergeStyles({
                    ...expandStyles,
                    willChange: 'height',
                    display: 'block',
                    overflow: 'hidden',
                });
                innerAnimationFrameTimer = window.requestAnimationFrame(() => {
                    const height = getElementHeight(el);
                    mergeStyles({
                        ...getTransitionStyles(height),
                        overflow: 'visible',
                        height,
                    });
                });
            });
        }
        else {
            animationFrameTimer = window.requestAnimationFrame(() => {
                onCollapseStart();
                const height = getElementHeight(el);
                mergeStyles({
                    ...collapseStyles,
                    ...getTransitionStyles(height),
                    willChange: 'height',
                    height,
                });
                innerAnimationFrameTimer = window.requestAnimationFrame(() => {
                    mergeStyles({
                        height: collapsedHeight,
                        overflow: 'hidden',
                    });
                });
            });
        }
        return () => {
            window.cancelAnimationFrame(animationFrameTimer);
            window.cancelAnimationFrame(innerAnimationFrameTimer);
        };
    }, [isExpanded, collapsedHeight]);
    const handleTransitionEnd = (e) => {
        // Sometimes onTransitionEnd is triggered by another transition,
        // such as a nested collapse panel transitioning. But we only
        // want to handle this if this component's element is transitioning
        if (e.target !== el.current || e.propertyName !== 'height') {
            return;
        }
        // The height comparisons below are a final check before
        // completing the transition
        // Sometimes this callback is run even though we've already begun
        // transitioning the other direction
        // The conditions give us the opportunity to bail out,
        // which will prevent the collapsed content from flashing on the screen
        if (isExpanded) {
            const height = getElementHeight(el);
            // If the height at the end of the transition
            // matches the height we're animating to,
            if (height === styles.height) {
                setStyles({});
            }
            else {
                // If the heights don't match, this could be due the height
                // of the content changing mid-transition
                mergeStyles({ height });
            }
            onExpandEnd();
            // If the height we should be animating to matches the collapsed height,
            // it's safe to apply the collapsed overrides
        }
        else if (styles.height === collapsedHeight) {
            setStyles(collapsedStyles);
            setExitAnimationEnded(true);
            onCollapseEnd();
        }
    };
    function getCollapseProps({ style = {}, onTransitionEnd = noop, refKey = 'ref', ...rest } = {}) {
        const theirRef = rest[refKey];
        return {
            id: `react-collapsed-panel-${uniqueId}`,
            'aria-hidden': !isExpanded,
            ...rest,
            [refKey]: mergeRefs(el, theirRef),
            onTransitionEnd: callAll(handleTransitionEnd, onTransitionEnd),
            style: {
                boxSizing: 'border-box',
                // additional styles passed, e.g. getCollapseProps({style: {}})
                ...style,
                // style overrides from state
                ...styles,
            },
        };
    }
    return {
        getCollapseProps,
        isExpanded,
        setExpanded,
        exitAnimationEnded: process.env.NODE_ENV === 'test' ? !isExpanded : exitAnimationEnded,
    };
}
//# sourceMappingURL=useCollapse.js.map