import { clsx } from 'clsx';
import {
  CSSProperties,
  HTMLAttributes,
  PropsWithChildren,
  SyntheticEvent,
  useContext,
  useRef,
  useState,
} from 'react';

import Dimmer from '../../dimmer';
import Drawer from '../../drawer';
import { OverlayIdContext } from '../../provider/overlay/OverlayIdProvider';
import SlidingPanel from '../../slidingPanel';
import { CloseButton } from '../closeButton';
import { CommonProps } from '../commonProps';
import { isServerSide } from '../domHelpers';
import { useConditionalListener } from '../hooks';
import { useMedia } from '../hooks/useMedia';
import { Breakpoint } from '../propsValues/breakpoint';
import { Position } from '../propsValues/position';

const INITIAL_Y_POSITION = 0;

const CONTENT_SCROLL_THRESHOLD = 1;

const MOVE_OFFSET_THRESHOLD = 50;

export type BottomSheetProps = PropsWithChildren<
  {
    onClose?: (event: Event | SyntheticEvent) => void;
    open: boolean;
  } & CommonProps &
    Pick<HTMLAttributes<HTMLDivElement>, 'role' | 'aria-labelledby' | 'aria-label'>
>;

/**
 * Neptune: https://transferwise.github.io/neptune/components/bottom-sheet/
 *
 * Neptune Web: https://transferwise.github.io/neptune-web/components/overlays/BottomSheet
 *
 */
const BottomSheet = ({ role = 'dialog', ...props }: BottomSheetProps) => {
  const bottomSheetReference = useRef<HTMLDivElement>(null);
  const topBarReference = useRef<HTMLDivElement>(null);
  const contentReference = useRef<HTMLDivElement>(null);

  const [pressed, setPressed] = useState<boolean>(false);

  /**
   * Used to track `requestAnimationFrame` requests
   *
   * @see https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame#return_value
   */
  const animationId = useRef<number>(0);

  /**
   * Difference between initial coordinate ({@link initialYCoordinate})
   * and new position (when user moves component), it's get calculated on `onTouchMove` and `onMouseMove` events
   *
   * @see {@link calculateOffsetAfterMove}
   */
  const moveOffset = useRef<number>(0);
  const initialYCoordinate = useRef<number>(0);

  // apply shadow to the bottom of top-bar when scroll over content
  useConditionalListener({
    attachListener: props.open && !isServerSide(),
    callback: () => {
      if (topBarReference.current !== null) {
        const { classList } = topBarReference.current;
        if (!isContentScrollPositionAtTop()) {
          classList.add('np-bottom-sheet--top-bar--shadow');
        } else {
          classList.remove('np-bottom-sheet--top-bar--shadow');
        }
      }
    },
    eventType: 'scroll',
    parent: isServerSide() ? undefined : document,
  });

  function move(newHeight: number): void {
    if (bottomSheetReference.current !== null) {
      bottomSheetReference.current.style.transform = `translateY(${newHeight}px)`;
    }
  }

  function close(event: Event | SyntheticEvent): void {
    setPressed(false);
    moveOffset.current = INITIAL_Y_POSITION;
    if (bottomSheetReference.current !== null) {
      bottomSheetReference.current.style.removeProperty('transform');
    }
    if (props.onClose) {
      props.onClose(event);
    }
  }

  const onSwipeStart = (
    event: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
  ): void => {
    initialYCoordinate.current = ('touches' in event ? event.touches[0] : event).clientY;
    setPressed(true);
  };

  const onSwipeMove = (
    event: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
  ): void => {
    if (pressed) {
      const { clientY } = 'touches' in event ? event.touches[0] : event;

      const offset = calculateOffsetAfterMove(clientY);
      // check whether move is to the bottom only and content scroll position is at the top
      if (offset > INITIAL_Y_POSITION && isContentScrollPositionAtTop()) {
        moveOffset.current = offset;
        animationId.current = requestAnimationFrame(() => {
          if (animationId.current !== undefined && bottomSheetReference.current !== null) {
            move(offset);
          }
        });
      }
    }
  };

  function onSwipeEnd(
    event: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
  ): void {
    // stop moving component
    cancelAnimationFrame(animationId.current);
    setPressed(false);
    // check whether move down is strong enough
    // and content scroll position is at the top to close the component
    if (moveOffset.current > MOVE_OFFSET_THRESHOLD && isContentScrollPositionAtTop()) {
      close(event);
    }
    // otherwise move component back to default (initial) position
    else {
      move(INITIAL_Y_POSITION);
    }
    moveOffset.current = INITIAL_Y_POSITION;
  }

  function isContentScrollPositionAtTop(): boolean {
    return (
      contentReference?.current?.scrollTop !== undefined &&
      contentReference.current.scrollTop <= CONTENT_SCROLL_THRESHOLD
    );
  }

  /**
   * Calculates how hard user moves component,
   * result value used to determine whether to hide component or re-position to default state
   *
   * @param afterMoveYCoordinate
   */
  function calculateOffsetAfterMove(afterMoveYCoordinate: number): number {
    return afterMoveYCoordinate - initialYCoordinate.current;
  }

  /**
   * Set `max-height` for content part (in order to keep it scrollable for content overflow cases) of the component
   * and ensures space for safe zone (32px) at the top.
   */
  function setContentMaxHeight(): CSSProperties {
    const safeZoneHeight = '64px';
    const topbarHeight = '32px';
    const windowHight: number = isServerSide() ? 0 : window.innerHeight;
    /**
     * Calculate _real_ height of the screen (taking into account parts of browser interface).
     *
     * See https://css-tricks.com/the-trick-to-viewport-units-on-mobile for more details.
     */
    const screenHeight = `${windowHight * 0.01 * 100}px`;
    return {
      maxHeight: `calc(${screenHeight} - ${safeZoneHeight} - ${topbarHeight})`,
    };
  }

  const is400Zoom = useMedia(`(max-width: ${Breakpoint.ZOOM_400}px)`);

  const overlayId = useContext(OverlayIdContext);

  return is400Zoom ? (
    <Drawer
      aria-labelledby={props['aria-labelledby']}
      aria-label={props['aria-label']}
      role={role}
      open={props.open}
      className={props.className}
      onClose={close}
    >
      {props.children}
    </Drawer>
  ) : (
    <Dimmer open={props.open} fadeContentOnEnter fadeContentOnExit onClose={close}>
      <SlidingPanel
        ref={bottomSheetReference}
        open={props.open}
        position={Position.BOTTOM}
        className={clsx('np-bottom-sheet', props.className)}
      >
        <div
          id={overlayId}
          aria-labelledby={props['aria-labelledby'] || undefined}
          aria-label={props['aria-label'] || undefined}
          role={role}
          aria-modal
          onTouchStart={onSwipeStart}
          onTouchMove={onSwipeMove}
          onTouchEnd={onSwipeEnd}
          onMouseDown={onSwipeStart}
          onMouseMove={onSwipeMove}
          onMouseUp={onSwipeEnd}
        >
          <div ref={topBarReference} className="np-bottom-sheet--top-bar">
            <div className="np-bottom-sheet--handler" />
            <CloseButton size="sm" className="sr-only np-bottom-sheet--close-btn" onClick={close} />
          </div>
          <div
            ref={contentReference}
            style={setContentMaxHeight()}
            className="np-bottom-sheet--content"
          >
            {props.children}
          </div>
        </div>
      </SlidingPanel>
    </Dimmer>
  );
};

export default BottomSheet;
