/**
 * TEAM: frontend_infra
 * @flow strict
 */

import * as React from "react";
import {StyleSheet, css} from "aphrodite";
// eslint-disable-next-line flexport/no-legacy-dependencies
import {zIndices} from "latitude/tools/deprecatedZIndices";
import Loader from "latitude/Loader";
import colors from "./colors";
import PopupWithClickAway from "./popup/PopupWithClickAway";
import StackingContext from "./StackingContext";

// Gap between parent and dropdown
const DROPDOWN_GAP = 8;

type Props = {|
  /** The content for the list to display */
  +children: React.Node,
  /** Parent of the dropdown, interacting with this element will open, close, and affect the dropdown. */
  +parent: React.Node,
  /** The max height of the dropdown displayed before scrolling */
  +maxHeight?: number,
  /** Handler for when a key is pressed with the given parent focused */
  +onKeyDown?: (e: KeyboardEvent) => void,
  /** Callback when dropdown opens */
  +onOpen?: () => void,
  /** Callback when dropdown closes */
  +onClose?: () => void,
|};

export type DropdownInstance = {|
  +show: () => void,
  +hide: () => void,
  +delayedHide: () => void,
|};

/**
 * @category Data Entry
 * @short Attaches a dropdown to a given parent with a unified focus state. Consider implementing this as a hook in the future, especially after upgrading react-popper.
 * @brandStatus V2
 * @status In Review
 */
/** TODO: Consider implementing this as a hook in the future, especially after upgrading react-popper to v2 for hook usage. */
const Dropdown: React.AbstractComponent<Props, DropdownInstance> = React.memo<
  Props,
  DropdownInstance
>(
  React.forwardRef<Props, DropdownInstance>(function Dropdown(
    {children, parent, onKeyDown, onOpen, onClose}: Props,
    ref:
      | {current: DropdownInstance | null, ...}
      | ((inst: DropdownInstance | null) => mixed)
  ): React.Element<typeof PopupWithClickAway> {
    const {isDeprecatedZIndexEnabled} = React.useContext(StackingContext);
    const _popper = React.useRef();
    const parentWrapper = React.useRef();
    const delayedHideTimeoutId = React.useRef();
    // Use this to skip the first popover toggle since it conflicts with onFocus
    const toggleSkip = React.useRef(true);
    const show = React.useCallback(() => {
      const {current: popper} = _popper;
      if (popper == null) return;

      if (delayedHideTimeoutId.current) {
        clearTimeout(delayedHideTimeoutId.current);

        delayedHideTimeoutId.current = null;

        // If there is a delayedHide, that means we're switching from parent to dropdown, keep toggleSkip to false
        toggleSkip.current = false;
      }

      popper.openPopup();
    }, []);
    const hide = React.useCallback(() => {
      const {current: popper} = _popper;
      if (popper == null) return;

      if (delayedHideTimeoutId.current) {
        clearTimeout(delayedHideTimeoutId.current);

        delayedHideTimeoutId.current = null;
      }

      /** Ideally we would be using react-popper v2 hooks to get open/close
       * hooks but we haven't upgraded, alternatively we could be using the
       * function wrapping approach which works but this imperative approach is
       * easier to read and work with hooks */
      if (popper.state.isOpen) {
        popper.closePopup();

        toggleSkip.current = true;
      }
    }, []);
    /** We use a delayed hide since we'd like to cancel the hide if either the
     * dropdown or the dropdown parent is the next focused element. We cannot use
     * a wrapper element (I think) to treat the focus states of both elements as
     * one since one of the elements (the dropdown) is in a Portal. */
    const delayedHide = React.useCallback(() => {
      if (delayedHideTimeoutId.current) {
        clearTimeout(delayedHideTimeoutId.current);

        delayedHideTimeoutId.current = null;
      }

      toggleSkip.current = true;

      delayedHideTimeoutId.current = setTimeout(hide, 100);
    }, [hide]);
    const toggle = React.useCallback(() => {
      const {current: popper} = _popper;

      if (popper == null) return;

      if (delayedHideTimeoutId.current)
        clearTimeout(delayedHideTimeoutId.current);

      // Skip the first toggle since onFocus will already open the popover
      if (toggleSkip.current) {
        toggleSkip.current = false;
      } else {
        popper.togglePopup();
      }
    }, []);

    React.useImperativeHandle(ref, () => ({
      hide,
      show,
      delayedHide,
    }));

    return (
      <PopupWithClickAway
        ref={_popper}
        onClickOutside={() => {
          toggleSkip.current = true;
        }}
        onOpen={onOpen}
        onClose={onClose}
      >
        {(Target, Popup) => (
          <>
            <Target>
              <div
                className={css(styles.parentWrapper)}
                // `parent` itself should be tabbable (e.g. a button or input, so this element shouldn't be)
                tabIndex={-1}
                role="button"
                onClick={toggle}
                onFocus={show}
                onBlur={delayedHide}
                onKeyDown={onKeyDown}
                ref={parentWrapper}
              >
                <div className={css(styles.parent)}>{parent}</div>
              </div>
            </Target>
            <Popup
              placement="bottom-start"
              zIndex={
                isDeprecatedZIndexEnabled
                  ? zIndices.zIndex1500AboveModal.zIndex
                  : null
              }
            >
              <div
                onFocus={show}
                onBlur={delayedHide}
                className={css(styles.dropdown)}
                style={{
                  minWidth: parentWrapper.current?.offsetWidth,
                }}
              >
                <React.Suspense fallback={<Loader loaded={false} />}>
                  {children}
                </React.Suspense>
              </div>
            </Popup>
          </>
        )}
      </PopupWithClickAway>
    );
  })
);

export default Dropdown;

const styles = StyleSheet.create({
  parentWrapper: {
    outline: "none",
    // display: "inline-flex" makes the parent wrapper element size down to children
    display: "inline-flex",
    // fontSize: 0 removes the extra margin from `display: "inline-block"`
    fontSize: 0,
    letterSpacing: 0,
    wordSpacing: "0",
  },
  parent: {
    fontSize: 14,
    letterSpacing: "normal",
    wordSpacing: "normal",
  },
  dropdown: {
    pointerEvents: "all",
    position: "absolute",
    top: DROPDOWN_GAP,
    left: 0,
    backgroundColor: colors.white,
    boxShadow: "0px 0px 20px rgba(0, 0, 0, 0.12)",
  },
});
