import React__default, { useContext, useState, useRef, useCallback, useEffect, cloneElement } from 'react';
import { enabled } from '../../../feature-flags/es/index.js';
import ReactDOM__default from 'react-dom';
import { Tab } from './keyboard/keys.js';
import { match } from './keyboard/match.js';
import { selectorTabbable, selectorFocusable } from './keyboard/navigation.js';
import { OptimizedResize } from './OptimizedResize.js';
import { PrefixContext } from './usePrefix.js';
import { warning } from './warning.js';
import { wrapFocus, wrapFocusWithoutSentinels } from './wrapFocus.js';

/**
 * Copyright IBM Corp. 2016, 2023
 *
 * This source code is licensed under the Apache-2.0 license found in the
 * LICENSE file in the root directory of this source tree.
 */


const DIRECTION_LEFT = 'left';
const DIRECTION_TOP = 'top';
const DIRECTION_RIGHT = 'right';
const DIRECTION_BOTTOM = 'bottom';
/**
 * Computes the floating menu's position based on the menu size, trigger element
 * position, offset, direction, and container.
 */
const getFloatingPosition = ({
  menuSize,
  refPosition,
  offset,
  direction,
  scrollX,
  scrollY,
  container
}) => {
  const {
    left: refLeft = 0,
    top: refTop = 0,
    right: refRight = 0,
    bottom: refBottom = 0
  } = refPosition;
  const effectiveScrollX = container.position !== 'static' ? 0 : scrollX;
  const effectiveScrollY = container.position !== 'static' ? 0 : scrollY;
  const relativeDiff = {
    top: container.position !== 'static' ? container.rect.top : 0,
    left: container.position !== 'static' ? container.rect.left : 0
  };
  const {
    width,
    height
  } = menuSize;
  const {
    top = 0,
    left = 0
  } = offset;
  const refCenterHorizontal = (refLeft + refRight) / 2;
  const refCenterVertical = (refTop + refBottom) / 2;
  const positions = {
    [DIRECTION_LEFT]: () => ({
      left: refLeft - width + effectiveScrollX - left - relativeDiff.left,
      top: refCenterVertical - height / 2 + effectiveScrollY + top - 9 - relativeDiff.top
    }),
    [DIRECTION_TOP]: () => ({
      left: refCenterHorizontal - width / 2 + effectiveScrollX + left - relativeDiff.left,
      top: refTop - height + effectiveScrollY - top - relativeDiff.top
    }),
    [DIRECTION_RIGHT]: () => ({
      left: refRight + effectiveScrollX + left - relativeDiff.left,
      top: refCenterVertical - height / 2 + effectiveScrollY + top + 3 - relativeDiff.top
    }),
    [DIRECTION_BOTTOM]: () => ({
      left: refCenterHorizontal - width / 2 + effectiveScrollX + left - relativeDiff.left,
      top: refBottom + effectiveScrollY + top - relativeDiff.top
    })
  };
  return positions[direction]();
};
const FloatingMenu = ({
  children,
  flipped,
  focusTrap,
  menuDirection = DIRECTION_BOTTOM,
  menuOffset = {
    top: 0,
    left: 0
  },
  menuRef: externalMenuRef,
  onPlace,
  selectorPrimaryFocus,
  styles,
  target = () => document.body,
  triggerRef,
  updateOrientation
}) => {
  const prefix = useContext(PrefixContext);
  const [floatingPosition, setFloatingPosition] = useState(undefined);
  const menuBodyRef = useRef(null);
  const startSentinelRef = useRef(null);
  const endSentinelRef = useRef(null);
  const placeInProgressRef = useRef(false);
  const updateMenuPosition = useCallback(isAdjustment => {
    const menuBody = menuBodyRef.current;
    if (!menuBody) {
      warning(!!menuBody, 'The DOM node for menu body for calculating its position is not available. Skipping...') ;
      return;
    }
    const triggerEl = triggerRef.current;
    const menuSize = menuBody.getBoundingClientRect();
    const refPosition = triggerEl ? triggerEl.getBoundingClientRect() : undefined;
    const offsetValue = typeof menuOffset === 'function' ? menuOffset(menuBody, menuDirection, triggerEl, flipped) : menuOffset;
    const scrollX = globalThis.scrollX ?? 0;
    const scrollY = globalThis.scrollY ?? 0;
    if (updateOrientation) {
      updateOrientation({
        menuSize,
        refPosition,
        direction: menuDirection,
        offset: offsetValue,
        scrollX,
        scrollY,
        container: {
          rect: target().getBoundingClientRect(),
          position: getComputedStyle(target()).position
        }
      });
    }

    // Only set position if the menu has a valid size or if no offset is provided.
    if (menuSize.width > 0 && menuSize.height > 0 || !offsetValue) {
      const newFloatingPosition = getFloatingPosition({
        menuSize,
        refPosition: refPosition ?? {
          left: 0,
          top: 0,
          right: 0,
          bottom: 0
        },
        offset: offsetValue,
        direction: menuDirection,
        scrollX,
        scrollY,
        container: {
          rect: target().getBoundingClientRect(),
          position: getComputedStyle(target()).position
        }
      });

      // Only update if the position has actually changed.
      if (!floatingPosition || floatingPosition.left !== newFloatingPosition.left || floatingPosition.top !== newFloatingPosition.top) {
        setFloatingPosition(newFloatingPosition);
      }

      // Re-check after setting the position if not already adjusting.
      if (!isAdjustment) {
        const newMenuSize = menuBody.getBoundingClientRect();
        // TODO: Was there a bug in the old code? How could one `DOMRect` be
        // compared to another using `!==`?
        if (newMenuSize.width !== menuSize.width || newMenuSize.height !== menuSize.height) {
          updateMenuPosition(true);
        }
      }
    }
  }, [triggerRef, menuOffset, menuDirection, flipped, target, updateOrientation, floatingPosition]);
  const focusMenuContent = menuBody => {
    const primaryFocusNode = selectorPrimaryFocus ? menuBody.querySelector(selectorPrimaryFocus) : null;
    const tabbableNode = menuBody.querySelector(selectorTabbable);
    const focusableNode = menuBody.querySelector(selectorFocusable);
    const focusTarget = primaryFocusNode ||
    // User defined focusable node
    tabbableNode ||
    // First sequentially focusable node
    focusableNode ||
    // First programmatic focusable node
    menuBody;
    focusTarget.focus();
    if (focusTarget === menuBody) {
      warning(focusableNode === null, 'Floating Menus must have at least a programmatically focusable child. This can be accomplished by adding tabIndex="-1" to the content element.') ;
    }
  };
  const handleMenuRef = node => {
    menuBodyRef.current = node;
    placeInProgressRef.current = !!node;
    if (externalMenuRef) {
      externalMenuRef(node);
    }
    if (node) {
      updateMenuPosition();
    }
  };

  // When the menu has been placed, focus the content and call onPlace.
  useEffect(() => {
    if (placeInProgressRef.current && floatingPosition && menuBodyRef.current) {
      if (!menuBodyRef.current.contains(document.activeElement)) {
        focusMenuContent(menuBodyRef.current);
      }
      if (typeof onPlace === 'function') {
        onPlace(menuBodyRef.current);
      }
      placeInProgressRef.current = false;
    }
    // eslint-disable-next-line  react-hooks/exhaustive-deps -- https://github.com/carbon-design-system/carbon/issues/20452
  }, [floatingPosition, onPlace]);

  // Attach a resize listener.
  useEffect(() => {
    const resizeHandler = OptimizedResize.add(() => {
      updateMenuPosition();
    });
    return () => {
      resizeHandler.remove();
    };
    // eslint-disable-next-line  react-hooks/exhaustive-deps -- https://github.com/carbon-design-system/carbon/issues/20452
  }, [triggerRef, menuOffset, menuDirection, flipped, target, updateOrientation]);

  // Update menu position when key props change.
  useEffect(() => {
    updateMenuPosition();
    // eslint-disable-next-line  react-hooks/exhaustive-deps -- https://github.com/carbon-design-system/carbon/issues/20452
  }, [menuOffset, menuDirection, flipped, triggerRef, target, updateOrientation]);

  /**
   * Clones the child element to add a `ref` and positioning styles.
   */
  const getChildrenWithProps = () => {
    const pos = floatingPosition;
    const positioningStyle = pos ? {
      left: `${pos.left}px`,
      top: `${pos.top}px`,
      right: 'auto'
    } : {
      visibility: 'hidden',
      top: '0px'
    };
    const child = children;
    return /*#__PURE__*/cloneElement(child, {
      ref: handleMenuRef,
      style: {
        ...styles,
        ...positioningStyle,
        position: 'absolute',
        opacity: 1
      }
    });
  };

  /**
   * Blur handler used when focus trapping is enabled.
   */
  const handleBlur = event => {
    const {
      target,
      relatedTarget
    } = event;
    if (menuBodyRef.current && startSentinelRef.current && endSentinelRef.current && target instanceof HTMLElement && relatedTarget instanceof HTMLElement) {
      wrapFocus({
        bodyNode: menuBodyRef.current,
        startTrapNode: startSentinelRef.current,
        endTrapNode: endSentinelRef.current,
        currentActiveNode: relatedTarget,
        oldActiveNode: target,
        prefix
      });
    }
  };

  /**
   * Keydown handler for focus wrapping when experimental focus trap is enabled.
   */
  const handleKeyDown = event => {
    if (match(event, Tab) && menuBodyRef.current && event.target instanceof HTMLElement) {
      wrapFocusWithoutSentinels({
        containerNode: menuBodyRef.current,
        currentActiveNode: event.target,
        event
      });
    }
  };
  const deprecatedFlag = enabled('enable-experimental-focus-wrap-without-sentinels');
  const focusTrapWithoutSentinelsFlag = enabled('enable-focus-wrap-without-sentinels');
  const focusTrapWithoutSentinels = deprecatedFlag || focusTrapWithoutSentinelsFlag;
  if (typeof document !== 'undefined') {
    const portalTarget = target ? target() : document.body;
    return /*#__PURE__*/ReactDOM__default.createPortal(
    /*#__PURE__*/
    // eslint-disable-next-line  jsx-a11y/no-static-element-interactions  -- https://github.com/carbon-design-system/carbon/issues/20452
    React__default.createElement("div", {
      onBlur: focusTrap && !focusTrapWithoutSentinels ? handleBlur : undefined,
      onKeyDown: focusTrapWithoutSentinels ? handleKeyDown : undefined
    }, !focusTrapWithoutSentinels && /*#__PURE__*/React__default.createElement("span", {
      ref: startSentinelRef,
      tabIndex: 0,
      role: "link",
      className: `${prefix}--visually-hidden`
    }, "Focus sentinel"), getChildrenWithProps(), !focusTrapWithoutSentinels && /*#__PURE__*/React__default.createElement("span", {
      ref: endSentinelRef,
      tabIndex: 0,
      role: "link",
      className: `${prefix}--visually-hidden`
    }, "Focus sentinel")), portalTarget);
  }
  return null;
};

export { DIRECTION_BOTTOM, DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_TOP, FloatingMenu };
