import React, {
  cloneElement,
  Fragment,
  ReactElement,
  ReactNode,
  RefObject,
  useCallback,
  useId,
  useMemo,
} from 'react';
import {AnimatePresence} from 'framer-motion';
import {useControlledState} from '@react-stately/utils';
import {Options as OffsetOptions} from '@floating-ui/core/src/middleware/offset';
import {useLayoutEffect} from '@react-aria/utils';
import {useFloatingPosition} from '../floating-position';
import {useIsMobileMediaQuery} from '../../../utils/hooks/is-mobile-media-query';
import {DialogContext, DialogContextValue} from './dialog-context';
import {Popover} from '../popover';
import {Tray} from '../tray';
import {Modal} from '../modal';
import {createPortal} from 'react-dom';
import {createEventHandler} from '../../../utils/dom/create-event-handler';
import {Placement} from '@floating-ui/react-dom';
import {rootEl} from '../../../core/root-el';

type PopoverProps = {
  type: 'popover';
  placement?: Placement;
  offset?: OffsetOptions;
};
type ModalProps = {
  type: 'modal' | 'tray';
};
type Props = (PopoverProps | ModalProps) & {
  children: ReactNode;
  disableInitialTransition?: boolean;
  onClose?: (value?: any) => void;
  isDismissable?: boolean;
  isOpen?: boolean;
  onOpenChange?: (isOpen: boolean) => void;
  defaultIsOpen?: boolean;
  triggerRef?: RefObject<HTMLElement>;
  moveFocusToDialog?: boolean;
  returnFocusToTrigger?: boolean;
};
export function DialogTrigger(props: Props) {
  let {
    children,
    type,
    disableInitialTransition,
    onClose,
    isDismissable = true,
    triggerRef,
    moveFocusToDialog = true,
    returnFocusToTrigger = true,
  } = props;
  const [isOpen, setIsOpen] = useControlledState(
    props.isOpen,
    props.defaultIsOpen,
    props.onOpenChange
  );

  const {dialogTrigger, dialog} = extractChildren(children);

  // On small devices, show a modal or tray instead of a popover.
  const isMobile = useIsMobileMediaQuery();
  if (isMobile && type === 'popover') {
    type = 'modal';
  }

  const {x, y, reference, floating, strategy, refs} = useFloatingPosition({
    ...props,
    disablePositioning: type === 'modal',
  });

  const floatingStyle =
    type === 'popover'
      ? {
          position: strategy,
          top: y ?? '',
          left: x ?? '',
        }
      : {};

  const id = useId();
  const labelId = `${id}-label`;
  const descriptionId = `${id}-description`;
  const formId = `${id}-form`;

  const close = useCallback(
    (value?: any) => {
      onClose?.(value);
      setIsOpen(false);
    },
    [onClose, setIsOpen]
  );

  // position dropdown relative to provided ref, not the trigger
  useLayoutEffect(() => {
    if (triggerRef?.current && refs.reference.current !== triggerRef.current) {
      reference(triggerRef.current);
    }
  }, [reference, triggerRef, refs]);

  const dialogProps = useMemo(() => {
    return {
      'aria-labelledby': labelId,
      'aria-describedby': descriptionId,
    };
  }, [labelId, descriptionId]);

  let Overlay: typeof Modal | typeof Tray | typeof Popover;
  if (type === 'modal') {
    Overlay = Modal;
  } else if (type === 'tray') {
    Overlay = Tray;
  } else {
    Overlay = Popover;
  }

  const contextValue: DialogContextValue = useMemo(() => {
    return {
      dialogProps,
      type,
      labelId,
      descriptionId,
      isDismissable,
      close,
      formId,
    };
  }, [close, descriptionId, dialogProps, formId, labelId, type, isDismissable]);

  return (
    <Fragment>
      {dialogTrigger &&
        cloneElement(dialogTrigger, {
          onClick: createEventHandler((e: React.MouseEvent) => {
            // prevent propagating to parent, in case floating element
            // is attached to input field and button is inside the field
            e.stopPropagation();
            setIsOpen(!isOpen);
          }),
          ref: triggerRef ? null : reference,
          ...dialogTrigger.props,
        })}
      {rootEl &&
        createPortal(
          <AnimatePresence initial={!disableInitialTransition}>
            {isOpen && (
              <DialogContext.Provider value={contextValue}>
                <Overlay
                  ref={floating}
                  triggerRef={refs.reference as RefObject<HTMLElement>}
                  style={floatingStyle}
                  restoreFocus={returnFocusToTrigger}
                  autoFocus={moveFocusToDialog}
                  isOpen={isOpen}
                  onClose={close}
                  isDismissable={isDismissable}
                >
                  {dialog}
                </Overlay>
              </DialogContext.Provider>
            )}
          </AnimatePresence>,
          rootEl
        )}
    </Fragment>
  );
}

function extractChildren(rawChildren: ReactNode) {
  const children = React.Children.toArray(rawChildren);

  // trigger and dialog passed as children
  if (children && children.length === 2) {
    return {
      dialogTrigger: children[0] as ReactElement,
      dialog: children[1] as ReactElement,
    };
  }

  // only dialog passed as child
  return {dialog: children[0] as ReactElement};
}
