import {
  arrow as arrowMiddleware,
  autoUpdate,
  flip as flipMiddleware,
  offset,
  type Placement,
  shift,
  size as sizeMiddleware,
  useFloating,
} from '@floating-ui/react';
import { clsx } from 'clsx';
import {
  CSSProperties,
  HTMLAttributes,
  MutableRefObject,
  PropsWithChildren,
  SyntheticEvent,
  forwardRef,
  useContext,
  useRef,
} from 'react';

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

const POPOVER_OFFSET = 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: Record<string, Placement[]> = {
  [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 arrowRef = useRef<HTMLDivElement | null>(null);

  const middleware = [];

  if (arrow) {
    middleware.push(offset(POPOVER_OFFSET));
  }

  if (flip && fallbackPlacements[position]) {
    middleware.push(
      flipMiddleware({
        fallbackPlacements: fallbackPlacements[position],
      }),
    );
  }

  if (altAxis) {
    middleware.push(shift());
  }

  if (arrow) {
    middleware.push(arrowMiddleware({ element: arrowRef, padding: 8 }));
  }

  if (considerHeight) {
    middleware.push(
      sizeMiddleware({
        padding: POPOVER_OFFSET,
        apply: ({ elements, availableHeight }) => {
          elements.floating.style.setProperty(
            '--np-panel-available-height',
            `${availableHeight}px`,
          );
        },
      }),
    );
  }

  const { refs, floatingStyles, placement, middlewareData } = useFloating({
    placement: position,
    middleware,
    elements: {
      reference: anchorRef.current,
    },
    whileElementsMounted: open ? autoUpdate : undefined,
  });

  const arrowStyle: CSSProperties = {
    ...(middlewareData.arrow?.x != null ? { left: middlewareData.arrow.x } : {}),
    ...(middlewareData.arrow?.y != null ? { top: middlewareData.arrow.y } : {}),
  };

  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={refs.setFloating}
        role="dialog"
        style={floatingStyles}
        data-popper-placement={placement}
        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={arrowRef} className={clsx('np-panel__arrow')} style={arrowStyle} />}
        </div>
      </div>
    </Dimmer>
  );
});

export default Panel;
