import { clsx } from 'clsx';
import {
  CSSProperties,
  HTMLAttributes,
  MutableRefObject,
  PropsWithChildren,
  SyntheticEvent,
  forwardRef,
  useContext,
  useEffect,
  useState,
} from 'react';
import { usePopper } from 'react-popper';

import { Position, PositionBottom, PositionLeft, PositionRight, PositionTop } from '..';
import Dimmer from '../../dimmer';
import { OverlayIdContext } from '../../provider/overlay/OverlayIdProvider';

const POPOVER_OFFSET = [0, 16];

// By default the flip positioning explores only the opposite alternative. So if left is passed and there's no enough space
// the right one gets chosen. If there's no space on both sides popover goes back to the initially chosen one left.
// This mapping forces popover to try the four available positions before going back to the initial chosen one.
const fallbackPlacements = {
  [Position.TOP]: [Position.BOTTOM, Position.RIGHT, Position.LEFT],
  [Position.BOTTOM]: [Position.TOP, Position.RIGHT, Position.LEFT],
  [Position.LEFT]: [Position.RIGHT, Position.TOP, Position.BOTTOM],
  [Position.RIGHT]: [Position.LEFT, Position.TOP, Position.BOTTOM],
};

export type PanelProps = PropsWithChildren<{
  arrow?: boolean;
  flip?: boolean;
  altAxis?: boolean;
  open?: boolean;
  onClose?: (event: Event | SyntheticEvent) => void;
  position?: PositionBottom | PositionLeft | PositionRight | PositionTop;
  anchorRef: MutableRefObject<Element | null>;
  anchorWidth?: boolean;
  considerHeight?: boolean;
}> &
  HTMLAttributes<HTMLDivElement>;

const Panel = forwardRef<HTMLDivElement, PanelProps>(function Panel(
  {
    arrow = false,
    flip = true,
    altAxis = false,
    children,
    open = false,
    onClose,
    position = Position.BOTTOM,
    anchorRef,
    anchorWidth = false,
    considerHeight = false,
    ...rest
  }: PanelProps,
  reference,
) {
  const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null);
  const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);

  const modifiers = [];

  if (altAxis) {
    modifiers.push({
      // https://popper.js.org/docs/v2/modifiers/prevent-overflow
      name: 'preventOverflow',
      options: {
        altAxis: true,
        tether: false,
      },
    });
  }

  if (arrow) {
    modifiers.push({
      name: 'arrow',
      options: {
        element: arrowElement,
        options: {
          padding: 8, // 8px from the edges of the popper
        },
      },
    });
    // This lets you displace a popper element from its reference element.
    modifiers.push({ name: 'offset', options: { offset: POPOVER_OFFSET } });
  }
  if (flip && fallbackPlacements[position]) {
    modifiers.push({
      name: 'flip',
      options: {
        fallbackPlacements: fallbackPlacements[position],
      },
    });
  }

  const { styles, attributes, forceUpdate } = usePopper(anchorRef.current, popperElement, {
    placement: position,
    modifiers,
  });

  // If the trigger is not visible when the position is calculated, it will be incorrect. Because this can happen repeatedly (on resize for example),
  // it is most simple just to always position before opening
  useEffect(() => {
    if (open && forceUpdate) {
      forceUpdate();
    }
  }, [open]);

  const contentStyle: CSSProperties = {
    ...(anchorWidth ? { width: anchorRef.current?.clientWidth } : undefined),
  };

  const overlayId = useContext(OverlayIdContext);

  return (
    <Dimmer open={open} transparent fadeContentOnEnter fadeContentOnExit onClose={onClose}>
      <div
        id={overlayId}
        {...rest}
        ref={setPopperElement}
        role="dialog"
        style={{ ...styles.popper }}
        {...attributes.popper}
        className={clsx('np-panel', { 'np-panel--open': open }, rest.className)}
      >
        <div ref={reference} style={contentStyle} className={clsx('np-panel__content')}>
          {children}
          {/* Arrow has to stay inside content to get the same animations as the "dialog" and to get hidden when panel is closed. */}
          {arrow && (
            <div ref={setArrowElement} className={clsx('np-panel__arrow')} style={styles.arrow} />
          )}
        </div>
      </div>
    </Dimmer>
  );
});

export default Panel;
