// Adapted from: https://github.com/segmentio/evergreen/blob/26d59831d28f44dd772b78c95364ce5f46c453b8/src/positioner/src/Positioner.js
import { bool, func, number, oneOf, object } from 'prop-types';
import * as React from 'react';
import Portal from '../Portal';
import { getPosition, Position } from './util';
import { useZIndex, ZIndexProvider } from '../ZIndex';

const initialState = {
  top: null,
  left: null,
  transformOrigin: null,
};

const Positioner = ({
  shown,
  renderContent,
  children,
  position,
  targetOffset,
  bodyOffset,
  contentRef: contentRefProp,
  targetRef: targetRefProp,
}) => {
  const [state, setState] = React.useState(initialState);
  const defaultContentRef = React.useRef();
  const defaultTargetRef = React.useRef();
  const parentZIndex = useZIndex();
  const zIndex = parentZIndex + 5;

  const contentRef = contentRefProp || defaultContentRef;
  const targetRef = targetRefProp || defaultTargetRef;

  function update() {
    if (!shown || !targetRef.current || !contentRef.current) return;

    const targetRect = targetRef.current.getBoundingClientRect();

    const viewportHeight = document.documentElement.clientHeight;
    const viewportWidth = document.documentElement.clientWidth;

    const contentRect = contentRef.current.getBoundingClientRect();

    // We need to round the width and height to prevent jitter when
    // the window is zoomed (when `window.devicePixelRatio` is not an integer)
    const height = Math.round(contentRect.height);
    const width = Math.round(contentRect.width);

    const { rect, transformOrigin } = getPosition({
      position,
      targetRect,
      targetOffset,
      dimensions: {
        height,
        width,
      },
      viewport: {
        width: viewportWidth,
        height: viewportHeight,
      },
      viewportOffset: bodyOffset,
    });

    setState({
      left: rect.left,
      top: rect.top,
      transformOrigin,
    });
  }

  const { top, left, transformOrigin } = state;

  React.useLayoutEffect(() => {
    const rafId = requestAnimationFrame(update);
    return () => {
      cancelAnimationFrame(rafId);
    };
  });

  return (
    <ZIndexProvider value={zIndex}>
      {children?.({ ref: targetRef })}
      {shown && (
        <Portal>
          {renderContent({
            top,
            left,
            style: {
              position: 'fixed',
              transformOrigin,
              left,
              top,
              zIndex,
              visibility: shown ? 'visible' : 'hidden',
            },
            ref: contentRef,
          })}
        </Portal>
      )}
    </ZIndexProvider>
  );
};

Positioner.propTypes = {
  /**
   * The position the element that is being positioned is on.
   * Smart positioning might override this.
   */
  position: oneOf(Object.values(Position)),

  /**
   * When true, show the element being positioned.
   */
  shown: bool,

  /**
   * The minimum distance from the body to the element being positioned.
   */
  bodyOffset: number,

  /**
   * The minimum distance from the target to the element being positioned.
   */
  targetOffset: number,

  /**
   * Function that should return a node for the target.
   * ({ ref }) -> React Node
   */
  children: func,

  /**
   * Function that returns the element being positioned.
   * ({ ref, style }) -> React Node
   */
  renderContent: func.isRequired,

  targetRef: object,

  contentRef: object,

  zIndex: number,
};

Positioner.defaultProps = {
  shown: false,
  position: Position.BOTTOM,
  bodyOffset: 6,
  targetOffset: 6,
  zIndex: 10,
  children: undefined,
  targetRef: undefined,
  contentRef: undefined,
};

export default Positioner;
export { Position };
