import React, {
  FC,
  ReactElement,
  cloneElement,
  forwardRef,
  ReactNode,
  useLayoutEffect,
} from "react";
import classNames from "classnames";
import {
  Manager as PopperManager,
  Reference as PopperReference,
  Popper,
  PopperArrowProps,
} from "react-popper-2";
import { Box, BoxProps } from "../Box";
import { Portal } from "../Portal";
import { bemHOF } from "../../utilities/bem";
import { PopperPlacement } from "../../types";

const cn = bemHOF("Popover");

interface InlineSharedProps {
  /**
   * Removes the padding from inside the popover
   */
  removePadding?: boolean;
}

export interface PopoverTooltipSharedProps extends BoxProps {
  /**
   * The element that the popover should be attached to. The component
   * as `referenceElement` must be able to accept a `ref`. If you’re
   * using a component that does not allow ref forwarding, wrap the
   * component in a `Box`.
   */
  referenceElement: ReactElement<any>;
  /**
   * Position where the popover should appear
   */
  placement?: PopperPlacement;
  /**
   * Array of Popper.js modifiers
   */
  popperModifiers?: Record<string, unknown>[];
  /**
   * Manually control the popover’s visibility
   */
  isVisible?: boolean;
}

interface PopoverDefaultProps
  extends PopoverTooltipSharedProps,
    InlineSharedProps {
  /**
   * Whether or not to render the popover inline
   */
  inline?: false;
  /**
   * Toggles the popover arrow on and off
   */
  showArrow?: boolean;
}

interface PopoverInlineProps extends InlineSharedProps, BoxProps {
  inline: true;
  placement?: "top" | "right" | "bottom" | "left";
}

interface InlinePopoverProps extends BoxProps {
  "data-placement"?: PopperPlacement;
  hasPortal?: boolean;
  removePadding: boolean;
}

type PopoverProps = PopoverDefaultProps | PopoverInlineProps;

const InlinePopover: FC<InlinePopoverProps> = forwardRef<
  HTMLDivElement,
  InlinePopoverProps
>(({ className, hasPortal = false, removePadding, ...rest }, ref) => {
  const Container = hasPortal ? Portal : React.Fragment;

  return (
    <Container>
      <Box
        ref={ref}
        className={classNames(cn(), className, {
          [cn({ m: "padded" })]: !removePadding,
        })}
        {...rest}
      />
    </Container>
  );
});

const ChildrenWithArrow = ({
  children,
  popperPlacement,
  arrowProps,
  update,
  showArrow,
}: {
  children: ReactNode;
  popperPlacement: PopperPlacement;
  arrowProps: PopperArrowProps;
  update: () => void;
  showArrow: boolean;
}) => {
  useLayoutEffect(() => {
    update();
  }, [children, update]);

  return (
    <>
      {children}
      {showArrow && (
        <div
          className={classNames(cn({ e: "arrow" }))}
          data-placement={popperPlacement}
          {...arrowProps}
        />
      )}
    </>
  );
};

export const Popover = ({ removePadding = false, ...props }: PopoverProps) => {
  if (props.inline) {
    const {
      placement,
      className,
      children,
      style: styleProp,
      inline,
      ...rest
    } = props;
    return (
      <InlinePopover
        data-placement={placement}
        className={className}
        style={styleProp}
        removePadding={removePadding}
        {...rest}
      >
        {children}
      </InlinePopover>
    );
  }

  if (!props.inline) {
    const {
      referenceElement,
      popperModifiers,
      isVisible,
      placement,
      className,
      style: styleProp,
      children,
      inline,
      showArrow = false,
      ...rest
    } = props;

    const offset = showArrow ? [0, 18] : [0, 8];

    const defaultPopperModifiers = [
      {
        name: "offset",
        options: {
          offset,
        },
      },
    ];

    const propModifiers = popperModifiers || [];

    return (
      <PopperManager>
        {isVisible && (
          <Popper
            placement={placement}
            modifiers={[...defaultPopperModifiers, ...propModifiers]}
          >
            {({
              ref,
              style,
              placement: popperPlacement,
              arrowProps,
              update,
            }) => (
              <InlinePopover
                ref={ref}
                style={{
                  ...style,
                  ...styleProp,
                }}
                className={className}
                data-placement={popperPlacement}
                hasPortal
                removePadding={removePadding}
                {...rest}
              >
                <ChildrenWithArrow
                  popperPlacement={popperPlacement}
                  arrowProps={arrowProps}
                  update={update}
                  showArrow={showArrow}
                >
                  {children}
                </ChildrenWithArrow>
              </InlinePopover>
            )}
          </Popper>
        )}

        <PopperReference>
          {({ ref }) =>
            cloneElement(referenceElement, {
              ref,
            })
          }
        </PopperReference>
      </PopperManager>
    );
  }

  return null;
};
