import _defineProperty from "@babel/runtime/helpers/defineProperty";
import React from 'react';
import { bind } from 'bind-event-listener';
import createFocusTrap from 'focus-trap';
import rafSchedule from 'raf-schd';
import { createPortal, flushSync } from 'react-dom';
import { akEditorFloatingPanelZIndex } from '@atlaskit/editor-shared-styles';
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { calculatePlacement, calculatePosition, findOverflowScrollParent } from './utils';
import { validatePosition } from './validatePosition';
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/react/no-class-components
export default class Popup extends React.Component {
  constructor(...args) {
    var _window;
    super(...args);
    _defineProperty(this, "rafIds", new Set());
    _defineProperty(this, "state", {
      validPosition: true
    });
    _defineProperty(this, "popupRef", /*#__PURE__*/React.createRef());
    _defineProperty(this, "placement", ['', '']);
    _defineProperty(this, "handleRef", popup => {
      if (!popup) {
        return;
      }
      this.initPopup(popup);
    });
    _defineProperty(this, "scheduledUpdatePosition", rafSchedule(_props => {
      this.updatePosition(this.props);
    }));
    _defineProperty(this, "onResize", () => this.scheduledUpdatePosition(this.props));
    _defineProperty(this, "resizeObserver", (_window = window) !== null && _window !== void 0 && _window.ResizeObserver ? new ResizeObserver(() => {
      this.scheduledUpdatePosition(this.props);
    }) : undefined);
    /**
     * Raf scheduled so that it also occurs after the initial update position
     */
    _defineProperty(this, "initFocusTrap", rafSchedule(() => {
      const popup = this.popupRef.current;
      if (!popup) {
        return;
      }
      const defaultTrapConfig = {
        clickOutsideDeactivates: true,
        escapeDeactivates: true,
        initialFocus: popup,
        fallbackFocus: popup,
        returnFocusOnDeactivate: false
      };
      const trapConfig = typeof this.props.focusTrap === 'boolean' ? defaultTrapConfig : {
        ...defaultTrapConfig,
        ...this.props.focusTrap
      };
      this.focusTrap = createFocusTrap(popup, editorExperiment('platform_editor_block_menu', true) || expValEqualsNoExposure('platform_editor_layout_column_menu', 'isEnabled', true) ? trapConfig : defaultTrapConfig);
      this.focusTrap.activate();
    }));
  }
  /**
   * Calculates new popup position
   */
  calculatePosition(props, popup) {
    const {
      target,
      fitHeight,
      fitWidth,
      boundariesElement,
      minPopupMargin,
      offset,
      onPositionCalculated,
      onPlacementChanged,
      alignX,
      alignY,
      stick,
      forcePlacement,
      allowOutOfBounds,
      rect,
      preventOverflow,
      absoluteOffset
    } = props;
    if (!target || !popup) {
      return {};
    }
    const placement = calculatePlacement(target,
    // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- Existing Popup positioning fallback; keep unchanged for safety.
    boundariesElement || document.body, fitWidth, fitHeight, alignX, alignY, forcePlacement, preventOverflow);
    if (onPlacementChanged && this.placement.join('') !== placement.join('')) {
      onPlacementChanged(placement);
      this.placement = placement;
    }
    let position = calculatePosition({
      placement,
      popup,
      target,
      stick,
      // Ignored via go/ees005
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      offset: offset,
      allowOutOfBounds,
      rect,
      // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- Existing Popup positioning fallback; keep unchanged for safety.
      boundariesElement: boundariesElement || document.body,
      minPopupMargin,
      scrollableElement: stick ? this.scrollElement : undefined
    });
    position = onPositionCalculated ? onPositionCalculated(position) : position;
    if (typeof position.top !== 'undefined' && absoluteOffset !== null && absoluteOffset !== void 0 && absoluteOffset.top) {
      position.top = position.top + absoluteOffset.top;
    }
    if (typeof position.bottom !== 'undefined' && absoluteOffset !== null && absoluteOffset !== void 0 && absoluteOffset.bottom) {
      position.bottom = position.bottom + absoluteOffset.bottom;
    }
    if (typeof position.right !== 'undefined' && absoluteOffset !== null && absoluteOffset !== void 0 && absoluteOffset.right) {
      position.right = position.right + absoluteOffset.right;
    }
    if (typeof position.left !== 'undefined' && absoluteOffset !== null && absoluteOffset !== void 0 && absoluteOffset.left) {
      position.left = position.left + absoluteOffset.left;
    }
    return {
      position,
      validPosition: validatePosition(target)
    };
  }
  updatePosition(props = this.props, state = this.state) {
    const {
      popup
    } = state;
    const {
      position,
      validPosition
    } = this.calculatePosition(props, popup);
    if (position && validPosition) {
      flushSync(() => {
        this.setState({
          position,
          validPosition
        });
      });
    }
  }
  cannotSetPopup(popup, target, overflowScrollParent) {
    /**
     * Check whether:
     * 1. Popup's offset targets which means whether or not its possible to correctly position popup along with given target.
     * 2. Popup is inside "overflow: scroll" container, but its offset parent isn't.
     *
     * Currently Popup isn't capable of position itself correctly in case 2,
     * Add "position: relative" to "overflow: scroll" container or to some other FloatingPanel wrapper inside it.
     */
    return !target ||
    // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- Existing Popup containment check; keep unchanged for safety.
    document.body.contains(target) && popup.offsetParent &&
    // Ignored via go/ees005
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    !popup.offsetParent.contains(target) || overflowScrollParent && !overflowScrollParent.contains(popup.offsetParent);
  }

  /**
   * Popup initialization.
   * Checks whether it's possible to position popup along given target, and if it's not throws an error.
   */
  initPopup(popup) {
    if (this.popupRef.current) {
      var _this$resizeObserver;
      (_this$resizeObserver = this.resizeObserver) === null || _this$resizeObserver === void 0 ? void 0 : _this$resizeObserver.unobserve(this.popupRef.current);
    }
    this.popupRef.current = popup;
    const {
      target
    } = this.props;
    const overflowScrollParent = findOverflowScrollParent(popup);
    if (this.cannotSetPopup(popup, target, overflowScrollParent)) {
      return;
    }
    this.setState({
      popup
    });
    /**
     * Some plugins (like image) have async rendering of component in floating toolbar(which is popup).
     * Now, floating toolbar position depends on it's size.
     * Size of floating toolbar changes, when async component renders.
     * There is currently, no way to re position floating toolbar or
     *  better to not show floating toolbar till all the async component are ready to render.
     * Also, it is not even Popup's responsibility to take care of it as popup's children are passed
     *  as a prop.
     * So, calling scheduledUpdatePosition to position popup on next request animation frame,
     * which is currently working for most of the floating toolbar and other popups.
     */
    this.scheduledUpdatePosition(this.props);
    if (this.props.focusTrap) {
      this.initFocusTrap();
    }
    if (this.popupRef.current) {
      var _this$resizeObserver2;
      (_this$resizeObserver2 = this.resizeObserver) === null || _this$resizeObserver2 === void 0 ? void 0 : _this$resizeObserver2.observe(this.popupRef.current);
    }
  }
  /**
   * Cancels the initialisation of the focus trap if it has not yet occured
   * Deactivates the focus trap if it exists
   */
  destroyFocusTrap() {
    var _this$focusTrap;
    this.initFocusTrap.cancel();
    (_this$focusTrap = this.focusTrap) === null || _this$focusTrap === void 0 ? void 0 : _this$focusTrap.deactivate();
  }

  /**
   * Handle pausing, unpausing, and initialising (if not yet initialised) of the focus trap
   */
  handleChangedFocusTrapProp(prevProps) {
    if (prevProps.focusTrap !== this.props.focusTrap) {
      // If currently set to disable, then pause the trap if it exists
      if (!this.props.focusTrap) {
        var _this$focusTrap2;
        return (_this$focusTrap2 = this.focusTrap) === null || _this$focusTrap2 === void 0 ? void 0 : _this$focusTrap2.pause();
      }

      // If set to enabled and trap already exists, unpause
      if (this.focusTrap) {
        this.focusTrap.unpause();
      }

      // Else initialise the focus trap
      return this.initFocusTrap();
    }
  }

  /**
   * Idempotent scroll element setup — tears down any previous listener/observer
   * then attaches to the current scroll parent.
   */
  initScrollElement() {
    var _this$unbindScroll;
    const {
      stick,
      target,
      scrollableElement
    } = this.props;
    (_this$unbindScroll = this.unbindScroll) === null || _this$unbindScroll === void 0 ? void 0 : _this$unbindScroll.call(this);
    if (this.scrollElement && this.resizeObserver) {
      this.resizeObserver.unobserve(this.scrollElement);
    }
    this.scrollElement = scrollableElement;
    if (stick && !this.scrollElement && target) {
      this.scrollElement = findOverflowScrollParent(target);
    }
    if (this.scrollElement) {
      if (this.resizeObserver) {
        this.resizeObserver.observe(this.scrollElement);
      }
      this.unbindScroll = bind(this.scrollElement, {
        type: 'scroll',
        listener: this.onResize
      });
    }
  }
  componentDidUpdate(prevProps) {
    this.handleChangedFocusTrapProp(prevProps);
    if (prevProps.scrollableElement !== this.props.scrollableElement) {
      this.initScrollElement();
    }
    if (this.props !== prevProps) {
      this.scheduledUpdatePosition(prevProps);
    }
  }
  componentDidMount() {
    // Ignored via go/ees005
    // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
    window.addEventListener('resize', this.onResize);
    this.initScrollElement();
  }
  componentWillUnmount() {
    var _this$unbindScroll2, _this$resizeObserver3;
    // Ignored via go/ees005
    // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
    window.removeEventListener('resize', this.onResize);
    (_this$unbindScroll2 = this.unbindScroll) === null || _this$unbindScroll2 === void 0 ? void 0 : _this$unbindScroll2.call(this);
    if (this.scrollElement) {
      // Ignored via go/ees005
      // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
      this.scrollElement.removeEventListener('scroll', this.onResize);
    }
    (_this$resizeObserver3 = this.resizeObserver) === null || _this$resizeObserver3 === void 0 ? void 0 : _this$resizeObserver3.disconnect();
    this.scheduledUpdatePosition.cancel();
    this.destroyFocusTrap();
    const {
      onUnmount
    } = this.props;
    if (onUnmount) {
      onUnmount();
    }
  }
  renderPopup() {
    var _this$props$ariaLabel;
    const {
      position
    } = this.state;
    const {
      shouldRenderPopup
    } = this.props;
    if (shouldRenderPopup && !shouldRenderPopup(position || {})) {
      return null;
    }

    /**
     * https://a11y.atlassian.net/browse/A11Y-9995
     * We set aria-label to undefined if it's null, no more 'Popup' fallback.
     * It is meaningless for screen readers and causes confusion.
     */
    const ariaLabel = (_this$props$ariaLabel = this.props.ariaLabel) !== null && _this$props$ariaLabel !== void 0 ? _this$props$ariaLabel : undefined;
    const getRole = () => {
      // Provide a valid role only when aria-label is present to satisfy a11y rules, as when aria-label is present, role is required
      // use role = dialog as default role, as dialog role itself is not a parent role that requires specific children to function as some other ARIA roles(menu) do
      // if set default role to menu, tons of integration tests will fail as many of our popup usages do not have children that satisfy menu role requirements
      if (ariaLabel) {
        return this.props.role || 'dialog';
      }
      return undefined;
    };
    return /*#__PURE__*/React.createElement("div", {
      ref: this.handleRef,
      style: {
        // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
        position: 'absolute',
        // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766
        zIndex: this.props.zIndex || akEditorFloatingPanelZIndex,
        // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
        ...position,
        // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
        ...this.props.style
      },
      role: getRole(),
      "aria-label": ariaLabel,
      "data-testid": "popup-wrapper"
      // Indicates component is an editor pop. Required for focus handling in Message.tsx
      ,
      "data-editor-popup": true
    }, this.props.children);
  }
  render() {
    const {
      target,
      mountTo
    } = this.props;
    const {
      validPosition
    } = this.state;
    if (!target || !validPosition) {
      return null;
    }
    if (mountTo) {
      return /*#__PURE__*/createPortal(this.renderPopup(), mountTo);
    }

    // Without mountTo property renders popup as is,
    // which means it will be cropped by "overflow: hidden" container.
    return this.renderPopup();
  }
}
_defineProperty(Popup, "defaultProps", {
  offset: [0, 0],
  allowOutOfBound: false
});