import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
import React__default, { forwardRef, useContext, useState, useRef, useEffect, useCallback, Children, isValidElement, cloneElement } from 'react';
import { OverflowMenuVertical } from '@carbon/icons-react';
import classnames from 'classnames';
import invariant_1 from '../../../../../invariant/invariant.js';
import PropTypes from 'prop-types';
import { DIRECTION_TOP, DIRECTION_BOTTOM, FloatingMenu } from '../../internal/FloatingMenu.js';
import { ArrowUp, ArrowRight, ArrowDown, ArrowLeft, Escape } from '../../internal/keyboard/keys.js';
import { matches } from '../../internal/keyboard/match.js';
import { noopFn } from '../../internal/noopFn.js';
import { PrefixContext } from '../../internal/usePrefix.js';
import { deprecate } from '../../prop-types/deprecate.js';
import { mergeRefs } from '../../tools/mergeRefs.js';
import { setupGetInstanceId } from '../../tools/setupGetInstanceId.js';
import { IconButton } from '../IconButton/index.js';
import { useOutsideClick } from '../../internal/useOutsideClick.js';
import { deprecateValuesWithin } from '../../prop-types/deprecateValuesWithin.js';
import { mapPopoverAlign } from '../../tools/mapPopoverAlign.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 getInstanceId = setupGetInstanceId();
const on = (target, ...args) => {
  target.addEventListener(...args);
  return {
    release() {
      target.removeEventListener(...args);
      return null;
    }
  };
};

/**
 * The CSS property names of the arrow keyed by the floating menu direction.
 */
const triggerButtonPositionProps = {
  [DIRECTION_TOP]: 'bottom',
  [DIRECTION_BOTTOM]: 'top'
};

/**
 * Determines how the position of the arrow should affect the floating menu
 * position.
 */
const triggerButtonPositionFactors = {
  [DIRECTION_TOP]: -2,
  [DIRECTION_BOTTOM]: -1
};

/**
 * Calculates the offset for the floating menu.
 *
 * @param menuBody - The menu body with the menu arrow.
 * @param direction - The floating menu direction.
 * @returns The adjustment of the floating menu position, upon the position of
 *          the menu arrow.
 */
const getMenuOffset = (menuBody, direction, trigger, flip) => {
  const triggerButtonPositionProp = triggerButtonPositionProps[direction];
  const triggerButtonPositionFactor = triggerButtonPositionFactors[direction];
  {
    !(triggerButtonPositionProp && triggerButtonPositionFactor) ? invariant_1(false, '[OverflowMenu] wrong floating menu direction: `%s`', direction)  : void 0;
  }
  // eslint-disable-next-line @typescript-eslint/no-unused-vars -- https://github.com/carbon-design-system/carbon/issues/20452
  const {
    offsetWidth: menuWidth,
    offsetHeight: menuHeight
  } = menuBody;
  switch (triggerButtonPositionProp) {
    case 'top':
    case 'bottom':
      {
        // TODO: Ensure `trigger` is there for `<OverflowMenu open>`
        const triggerWidth = !trigger ? 0 : trigger.offsetWidth;
        return {
          left: (!flip ? 1 : -1) * (menuWidth / 2 - triggerWidth / 2),
          top: 0
        };
      }
    default:
      return {
        left: 0,
        top: 0
      };
  }
};
// eslint-disable-next-line react/display-name -- https://github.com/carbon-design-system/carbon/issues/20452
const OverflowMenu = /*#__PURE__*/forwardRef(({
  align,
  ['aria-label']: ariaLabel = null,
  ariaLabel: deprecatedAriaLabel,
  children,
  className,
  direction = DIRECTION_BOTTOM,
  flipped = false,
  focusTrap = true,
  iconClass,
  iconDescription = 'Options',
  id,
  light,
  menuOffset = getMenuOffset,
  menuOffsetFlip = getMenuOffset,
  menuOptionsClass,
  onClick = noopFn,
  onClose = noopFn,
  onOpen = noopFn,
  open: openProp,
  renderIcon: IconElement = OverflowMenuVertical,
  selectorPrimaryFocus = '[data-floating-menu-primary-focus]',
  size = 'md',
  innerRef,
  ...other
}, ref) => {
  const prefix = useContext(PrefixContext);
  const [open, setOpen] = useState(openProp ?? false);
  const [click, setClick] = useState(false);
  const [hasMountedTrigger, setHasMountedTrigger] = useState(false);
  /**  The handle of `onfocusin` or `focus` event handler. */
  const hFocusIn = useRef(null);
  const instanceId = useRef(getInstanceId());
  const menuBodyRef = useRef(null);
  const menuItemRefs = useRef({});
  const prevOpenProp = useRef(openProp);
  const prevOpenState = useRef(open);
  /** The element ref of the tooltip's trigger button. */
  const triggerRef = useRef(null);
  const wrapperRef = useRef(null);

  // Sync open prop changes.
  useEffect(() => {
    if (prevOpenProp.current !== openProp) {
      setOpen(!!openProp);
      prevOpenProp.current = openProp;
    }
  }, [openProp]);

  // Mark trigger as mounted.
  useEffect(() => {
    if (triggerRef.current) {
      setHasMountedTrigger(true);
    }
  }, []);
  useEffect(() => {
    if (open && !prevOpenState.current) {
      onOpen();
    } else if (!open && prevOpenState.current) {
      onClose();
    }
    prevOpenState.current = open;
  }, [open, onClose, onOpen]);
  useOutsideClick(wrapperRef, ({
    target
  }) => {
    if (open && (!menuBodyRef.current || target instanceof Node && !menuBodyRef.current.contains(target))) {
      closeMenu();
    }
  });
  const focusMenuEl = useCallback(() => {
    if (triggerRef.current) {
      triggerRef.current.focus();
    }
  }, []);
  const closeMenu = useCallback(onCloseMenu => {
    setOpen(false);
    // Optional callback to be executed after the state as been set to close
    if (onCloseMenu) {
      onCloseMenu();
    }
  }, []);
  const closeMenuAndFocus = useCallback(() => {
    const wasClicked = click;
    const wasOpen = open;
    closeMenu(() => {
      if (wasOpen && !wasClicked) {
        focusMenuEl();
      }
    });
  }, [click, open, closeMenu, focusMenuEl]);
  const closeMenuOnEscape = useCallback(() => {
    const wasOpen = open;
    closeMenu(() => {
      if (wasOpen) {
        focusMenuEl();
      }
    });
  }, [open, closeMenu, focusMenuEl]);
  const handleClick = evt => {
    setClick(true);
    if (!menuBodyRef.current || !menuBodyRef.current.contains(evt.target)) {
      setOpen(prev => !prev);
      onClick(evt);
    }
  };
  const handleKeyPress = evt => {
    if (open && matches(evt, [ArrowUp, ArrowRight, ArrowDown, ArrowLeft])) {
      evt.preventDefault();
    }

    // Close the overflow menu on escape
    if (matches(evt, [Escape])) {
      closeMenuOnEscape();

      // Stop the esc keypress from bubbling out and closing something it shouldn't
      evt.stopPropagation();
    }
  };

  /**
   * Focuses the next enabled overflow menu item given the currently focused
   * item index and direction to move.
   */
  const handleOverflowMenuItemFocus = ({
    currentIndex = 0,
    direction
  }) => {
    const enabledIndices = Children.toArray(children).reduce((acc, curr, i) => {
      if (/*#__PURE__*/React__default.isValidElement(curr) && !curr.props.disabled) {
        acc.push(i);
      }
      return acc;
    }, []);
    const nextValidIndex = (() => {
      const nextIndex = enabledIndices.indexOf(currentIndex) + direction;
      switch (nextIndex) {
        case -1:
          return enabledIndices.length - 1;
        case enabledIndices.length:
          return 0;
        default:
          return nextIndex;
      }
    })();
    const overflowMenuItem = menuItemRefs.current[enabledIndices[nextValidIndex]];
    overflowMenuItem?.focus();
  };
  const bindMenuBody = menuBody => {
    if (!menuBody) {
      menuBodyRef.current = menuBody;
    }
    if (!menuBody && hFocusIn.current) {
      hFocusIn.current = hFocusIn.current.release();
    }
  };
  const handlePlace = menuBody => {
    if (!menuBody) return;
    menuBodyRef.current = menuBody;
    const hasFocusin = 'onfocusin' in window;
    const focusinEventName = hasFocusin ? 'focusin' : 'focus';
    hFocusIn.current = on(menuBody.ownerDocument, focusinEventName, event => {
      const target = event.target;
      const triggerEl = triggerRef.current;
      if (typeof target.matches === 'function') {
        if (!menuBody.contains(target) && triggerEl && !target.matches(`.${prefix}--overflow-menu:first-child, .${prefix}--overflow-menu-options:first-child`)) {
          closeMenuAndFocus();
        }
      }
    }, !hasFocusin);
  };
  const getTarget = () => {
    const triggerEl = triggerRef.current;
    if (triggerEl instanceof Element) {
      return triggerEl.closest('[data-floating-menu-container]') || document.body;
    }
    return document.body;
  };
  const menuBodyId = `overflow-menu-${instanceId.current}__menu-body`;
  const overflowMenuClasses = classnames(className, `${prefix}--overflow-menu`, {
    [`${prefix}--overflow-menu--open`]: open,
    [`${prefix}--overflow-menu--light`]: light,
    [`${prefix}--overflow-menu--${size}`]: size
  });
  const overflowMenuOptionsClasses = classnames(menuOptionsClass, `${prefix}--overflow-menu-options`, {
    [`${prefix}--overflow-menu--flip`]: flipped,
    [`${prefix}--overflow-menu-options--open`]: open,
    [`${prefix}--overflow-menu-options--light`]: light,
    [`${prefix}--overflow-menu-options--${size}`]: size
  });
  const overflowMenuIconClasses = classnames(`${prefix}--overflow-menu__icon`, iconClass);
  const childrenWithProps = Children.toArray(children).map((child, index) => {
    if (/*#__PURE__*/isValidElement(child)) {
      const childElement = child;
      return /*#__PURE__*/cloneElement(childElement, {
        closeMenu: childElement.props.closeMenu || closeMenuAndFocus,
        handleOverflowMenuItemFocus,
        ref: el => {
          menuItemRefs.current[index] = el;
        },
        index
      });
    }
    return null;
  });
  const menuBody = /*#__PURE__*/React__default.createElement("ul", {
    className: overflowMenuOptionsClasses,
    tabIndex: -1,
    role: "menu",
    "aria-label": ariaLabel || deprecatedAriaLabel,
    onKeyDown: handleKeyPress,
    id: menuBodyId
  }, childrenWithProps);
  const wrappedMenuBody = /*#__PURE__*/React__default.createElement(FloatingMenu, {
    focusTrap: focusTrap,
    triggerRef: triggerRef,
    menuDirection: direction,
    menuOffset: flipped ? menuOffsetFlip : menuOffset,
    menuRef: bindMenuBody,
    flipped: flipped,
    target: getTarget,
    onPlace: handlePlace,
    selectorPrimaryFocus: selectorPrimaryFocus
  }, /*#__PURE__*/cloneElement(menuBody, {
    'data-floating-menu-direction': direction
  }));
  const combinedRef = innerRef ? mergeRefs(triggerRef, innerRef, ref) : mergeRefs(triggerRef, ref);
  return /*#__PURE__*/React__default.createElement(React__default.Fragment, null, /*#__PURE__*/React__default.createElement("span", {
    className: `${prefix}--overflow-menu__wrapper`,
    "aria-owns": open ? menuBodyId : undefined,
    ref: wrapperRef
  }, /*#__PURE__*/React__default.createElement(IconButton, _extends({}, other, {
    align: align,
    type: "button",
    "aria-haspopup": true,
    "aria-expanded": open,
    "aria-controls": open ? menuBodyId : undefined,
    className: overflowMenuClasses,
    onClick: handleClick,
    id: id,
    ref: combinedRef,
    size: size,
    label: iconDescription,
    kind: "ghost"
  }), /*#__PURE__*/React__default.createElement(IconElement, {
    className: overflowMenuIconClasses,
    "aria-label": iconDescription
  })), open && hasMountedTrigger && wrappedMenuBody));
});
OverflowMenu.propTypes = {
  /**
   * Specify how the trigger should align with the tooltip
   */
  align: deprecateValuesWithin(PropTypes.oneOf(['top', 'top-left',
  // deprecated use top-start instead
  'top-right',
  // deprecated use top-end instead

  'bottom', 'bottom-left',
  // deprecated use bottom-start instead
  'bottom-right',
  // deprecated use bottom-end instead

  'left', 'left-bottom',
  // deprecated use left-end instead
  'left-top',
  // deprecated use left-start instead

  'right', 'right-bottom',
  // deprecated use right-end instead
  'right-top',
  // deprecated use right-start instead

  // new values to match floating-ui
  'top-start', 'top-end', 'bottom-start', 'bottom-end', 'left-end', 'left-start', 'right-end', 'right-start']), ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end'], mapPopoverAlign),
  /**
   * Specify a label to be read by screen readers on the container node
   */
  ['aria-label']: PropTypes.string,
  /**
   * Deprecated, please use `aria-label` instead.
   * Specify a label to be read by screen readers on the container note.
   */
  ariaLabel: deprecate(PropTypes.string, 'This prop syntax has been deprecated. Please use the new `aria-label`.'),
  /**
   * The child nodes.
   */
  children: PropTypes.node,
  /**
   * The CSS class names.
   */
  className: PropTypes.string,
  /**
   * The menu direction.
   */
  direction: PropTypes.oneOf([DIRECTION_TOP, DIRECTION_BOTTOM]),
  /**
   * `true` if the menu alignment should be flipped.
   */
  flipped: PropTypes.bool,
  /**
   * Enable or disable focus trap behavior
   */
  focusTrap: PropTypes.bool,
  /**
   * The CSS class for the icon.
   */
  iconClass: PropTypes.string,
  /**
   * The icon description.
   */
  iconDescription: PropTypes.string,
  /**
   * The element ID.
   */
  id: PropTypes.string,
  /**
   * `true` to use the light version. For use on $ui-01 backgrounds only.
   * Don't use this to make OverflowMenu background color same as container background color.
   */
  light: deprecate(PropTypes.bool, 'The `light` prop for `OverflowMenu` is no longer needed and has been deprecated. It will be removed in the next major release. Use the Layer component instead.'),
  /**
   * The adjustment in position applied to the floating menu.
   */
  menuOffset: PropTypes.oneOfType([PropTypes.shape({
    top: PropTypes.number.isRequired,
    left: PropTypes.number.isRequired
  }), PropTypes.func]),
  /**
   * The adjustment in position applied to the floating menu.
   */
  menuOffsetFlip: PropTypes.oneOfType([PropTypes.shape({
    top: PropTypes.number.isRequired,
    left: PropTypes.number.isRequired
  }), PropTypes.func]),
  /**
   * The class to apply to the menu options
   */
  menuOptionsClass: PropTypes.string,
  /**
   * The event handler for the `click` event.
   */
  onClick: PropTypes.func,
  /**
   * Function called when menu is closed
   */
  onClose: PropTypes.func,
  /**
   * The event handler for the `focus` event.
   */
  onFocus: PropTypes.func,
  /**
   * The event handler for the `keydown` event.
   */
  onKeyDown: PropTypes.func,
  /**
   * Function called when menu is opened
   */
  onOpen: PropTypes.func,
  /**
   * `true` if the menu should be open.
   */
  open: PropTypes.bool,
  /**
   * A component used to render an icon.
   */
  renderIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
  /**
   * Specify a CSS selector that matches the DOM element that should
   * be focused when the OverflowMenu opens
   */
  selectorPrimaryFocus: PropTypes.string,
  /**
   * Specify the size of the OverflowMenu. Currently supports either `xs`, `sm`, `md` (default) or `lg` as an option.
   */
  size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg'])
};

export { OverflowMenu, OverflowMenu as default, getMenuOffset };
