import * as React from 'react';
import { useRef, useEffect } from 'react';
import debounce from 'lodash/debounce';
import batchUpdate from '../utils/batchUpdate';
import selectParent from '../utils/selectParent';
import useProperty from '../utils/useProperty';
import Overlay from './Overlay';
import join from '../utils/join';
import usePrevious from '../utils/usePrevious';
import { getDocRect, getRect } from './utils';
import useAgGridClassName from './useAgGridClassName';
import contains from '../utils/contains';
import { isBrowserDocumentAvailable } from '../../View/UIHelper';
import { useOverlay } from '../InfiniteTable';
import { useAdaptable } from '../../View/AdaptableContext';
import { OVERLAY_BASE_Z_INDEX } from '../overlayBaseZIndex';
export const getConstrainElement = (target, constrainTo) => {
    let el = null;
    if (typeof constrainTo === 'string') {
        el = selectParent(constrainTo, target);
    }
    if (typeof constrainTo === 'function') {
        el = constrainTo(target);
    }
    return el;
};
export const getConstrainRect = (target, constrainTo) => {
    let el = getConstrainElement(target, constrainTo);
    if (el && el.tagName) {
        return getRect(el);
    }
    return getDocRect();
};
let portalElement;
const OVERLAY_TRIGGER_Z_INDEX = OVERLAY_BASE_Z_INDEX;
export const ensurePortalElement = () => {
    if (!isBrowserDocumentAvailable()) {
        return;
    }
    if (portalElement) {
        return portalElement;
    }
    portalElement = document.createElement('div');
    portalElement.style.position = 'absolute';
    portalElement.style.zIndex = OVERLAY_TRIGGER_Z_INDEX.toString();
    portalElement.style.top = '0px';
    portalElement.style.left = '0px';
    portalElement.dataset.name = 'OverlayTriggerPortal';
    document.body.appendChild(portalElement);
    return portalElement;
};
const defaultProps = {
    showEvent: 'mouseenter',
    hideEvent: 'mouseleave',
    anchor: 'vertical',
    targetOffset: 10,
    opacityTransitionDuration: '250ms',
};
const OverlayTrigger = React.forwardRef((givenProps, ref) => {
    const props = { ...defaultProps, ...givenProps };
    const adaptable = useAdaptable();
    let { visible: _, showTriangle, showEvent, hideEvent, render, targetOffset, preventPortalEventPropagation = false, anchor, hideDelay = 0, opacityTransitionDuration, onVisibleChange, alignPosition = [
        // overlay - target
        ['TopLeft', 'BottomLeft'],
        ['TopRight', 'BottomRight'],
        ['TopCenter', 'BottomCenter'],
        ['BottomCenter', 'TopCenter'],
        ['TopRight', 'BottomCenter'],
        ['TopRight', 'BottomLeft'],
        ['TopRight', 'BottomLeft'],
        ['BottomLeft', 'TopLeft'],
        ['BottomRight', 'TopRight'],
        ['BottomRight', 'TopCenter'],
        ['BottomRight', 'TopLeft'],
        ['TopRight', 'CenterLeft'],
        ['TopRight', 'TopLeft'],
        ['TopLeft', 'TopRight'],
        ['CenterRight', 'CenterLeft'],
        ['CenterLeft', 'CenterRight'],
    ], constrainTo, target: targetProp, ...domProps } = props;
    const { showOverlay, clearAll: clearAllOverlays, hideOverlay, portal, } = useOverlay({
        portalContainer: portalElement,
    });
    const domRef = useRef(null);
    const targetRef = useRef(null);
    const overlayRef = useRef(null);
    const [visible, doSetVisible] = useProperty(props, 'visible', false);
    const hideTimeoutRef = useRef(null);
    const setVisible = React.useCallback(
    // visible state may quickly change from true -> false -> true
    // when moving the mouse cursor from the trigger to the overlay
    // for this case we debounce the visible change for a very small amount of time
    debounce((visible) => {
        onVisibleChange?.(visible);
        if (!visible) {
            hideTimeoutRef.current = setTimeout(() => {
                hideTimeoutRef.current = null;
                doSetVisible(false);
            }, hideDelay);
            return;
        }
        if (hideTimeoutRef.current) {
            clearTimeout(hideTimeoutRef.current);
            hideTimeoutRef.current = null;
        }
        doSetVisible(true);
    }, 50), []);
    const prevVisible = usePrevious(visible, false);
    ensurePortalElement();
    const onShow = React.useCallback((event) => {
        const target = targetRef.current;
        if (event && preventPortalEventPropagation && !contains(target, event.target)) {
            // because of how React portals behave - see https://github.com/facebook/react/issues/11387
            // after the portal is rendered, even though an item is in a separate dom parent, if it's in the portal,
            // events from it are propagated to the components in the portal and thus break some stuff
            return;
        }
        batchUpdate(() => {
            setVisible(true);
        }).commit();
    }, [constrainTo, preventPortalEventPropagation]);
    const onHide = React.useCallback((_event) => {
        const shouldHide = props.shouldHide ? props.shouldHide(_event) : true;
        if (shouldHide) {
            setVisible(false);
        }
    }, [setVisible]);
    useEffect(() => {
        if (ref) {
            const api = {
                show: onShow,
                hide: onHide,
            };
            if (typeof ref === 'function') {
                ref(api);
            }
            else {
                ref.current = api;
            }
        }
    }, [ref]);
    useEffect(() => {
        let target = domRef.current.previousSibling;
        if (targetProp) {
            target = targetProp(target);
        }
        if (!target) {
            adaptable.logger.warn('No OverlayTrigger target - make sure you render a child inside the OverlayTrigger, which will be the overlay target');
            return;
        }
        targetRef.current = target;
        let attached = false;
        let onShowFn = onShow;
        let onHideFn = onHide;
        if (props.visible === undefined) {
            attached = true;
            target.addEventListener(showEvent, onShowFn);
            target.addEventListener(hideEvent, onHideFn);
        }
        if (props.visible && !prevVisible) {
            onShowFn();
        }
        return () => {
            if (attached) {
                target.removeEventListener(showEvent, onShowFn);
                target.removeEventListener(hideEvent, onHideFn);
            }
        };
    }, [props.visible, showEvent, hideEvent, onShow, onHide]);
    React.useEffect(() => {
        const target = targetRef.current;
        if (!target) {
            return;
        }
        const targetWidth = target.getBoundingClientRect().width;
        if ((prevVisible && !visible) || visible) {
            const overlayContent = (React.createElement(Overlay, { ...domProps, ref: (node) => {
                    if (overlayRef.current && overlayRef.current != node) {
                        overlayRef.current.removeEventListener(showEvent, onShow);
                        overlayRef.current.removeEventListener(hideEvent, onHide);
                    }
                    overlayRef.current = node;
                    if (node) {
                        node.addEventListener(showEvent, onShow);
                        node.addEventListener(hideEvent, onHide);
                    }
                }, className: join('ab-Overlay', showTriangle ? 'ab-Overlay--show-triangle' : '', agGridClassName, domProps.className), style: { transition: `opacity ${opacityTransitionDuration}` }, visible: visible, onTransitionEnd: () => {
                    if (!visible) {
                        clearAllOverlays();
                        hideOverlay('overlay-trigger');
                    }
                } }, props.render({ targetWidth: targetWidth })));
            let preparedConstrainTo;
            if (constrainTo) {
                preparedConstrainTo = getConstrainElement(targetRef.current, constrainTo);
            }
            // show only if visible or if it was visible and now it is invisible
            const alignToRect = getRect(target, targetOffset ?? 0);
            const showOverlayOptions = {
                id: 'overlay-trigger', // add id on props so component is not unmounted
                alignPosition,
                constrainTo: preparedConstrainTo?.getBoundingClientRect?.() ?? true, // at least constrain to document
                alignTo: alignToRect,
            };
            showOverlay(() => overlayContent, showOverlayOptions);
        }
        else {
            clearAllOverlays();
        }
    }, [visible, props.render]);
    const agGridClassName = useAgGridClassName([visible]);
    return (React.createElement(React.Fragment, null,
        React.Children.only(props.children),
        React.createElement("div", { "data-name": "OverlayTrigger", "data-visible": visible, ref: domRef, style: {
                visibility: 'hidden',
                flex: 'none',
                width: 0,
                height: 0,
                pointerEvents: 'none',
                display: 'inline-flex',
            } }),
        portal));
});
export default OverlayTrigger;
