// @flow
import invariant from 'tiny-invariant';
import { useRef } from 'react';
import { useMemo, useCallback } from 'use-memo-one';
import type { Args, DragHandleProps } from './drag-handle-types';
import getWindowFromEl from '../window/get-window-from-el';
import useRequiredContext from '../use-required-context';
import AppContext, { type AppContextValue } from '../context/app-context';
import useMouseSensor, {
  type Args as MouseSensorArgs,
} from './sensor/use-mouse-sensor';
import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-target';
import useKeyboardSensor, {
  type Args as KeyboardSensorArgs,
} from './sensor/use-keyboard-sensor';
import useTouchSensor, {
  type Args as TouchSensorArgs,
} from './sensor/use-touch-sensor';
import usePreviousRef from '../use-previous-ref';
import { warning } from '../../dev-warning';
import useValidation from './use-validation';
import useFocusRetainer from './use-focus-retainer';
import useLayoutEffect from '../use-isomorphic-layout-effect';

function preventHtml5Dnd(event: DragEvent) {
  event.preventDefault();
}

type Capturing = {|
  abort: () => void,
|};

export default function useDragHandle(args: Args): ?DragHandleProps {
  // Capturing
  const capturingRef = useRef<?Capturing>(null);
  const onCaptureStart = useCallback((abort: () => void) => {
    invariant(
      !capturingRef.current,
      'Cannot start capturing while something else is',
    );
    capturingRef.current = {
      abort,
    };
  }, []);
  const onCaptureEnd = useCallback(() => {
    invariant(
      capturingRef.current,
      'Cannot stop capturing while nothing is capturing',
    );
    capturingRef.current = null;
  }, []);
  const abortCapture = useCallback(() => {
    invariant(capturingRef.current, 'Cannot abort capture when there is none');
    capturingRef.current.abort();
  }, []);

  const { canLift, style: styleContext }: AppContextValue = useRequiredContext(
    AppContext,
  );
  const {
    isDragging,
    isEnabled,
    draggableId,
    callbacks,
    getDraggableRef,
    getShouldRespectForcePress,
    canDragInteractiveElements,
  } = args;
  const lastArgsRef = usePreviousRef(args);

  useValidation({ isEnabled, getDraggableRef });

  const getWindow = useCallback(
    (): HTMLElement => getWindowFromEl(getDraggableRef()),
    [getDraggableRef],
  );

  const canStartCapturing = useCallback(
    (event: Event) => {
      // Cannot lift when disabled
      if (!isEnabled) {
        return false;
      }
      // Something on this element might be capturing.
      // A drag might not have started yet
      // We want to prevent anything else from capturing
      if (capturingRef.current) {
        return false;
      }

      // Do not drag if anything else in the system is dragging
      if (!canLift(draggableId)) {
        return false;
      }

      // Check if we are dragging an interactive element
      return shouldAllowDraggingFromTarget(event, canDragInteractiveElements);
    },
    [canDragInteractiveElements, canLift, draggableId, isEnabled],
  );

  const { onBlur, onFocus } = useFocusRetainer(args);

  const mouseArgs: MouseSensorArgs = useMemo(
    () => ({
      callbacks,
      getDraggableRef,
      getWindow,
      canStartCapturing,
      onCaptureStart,
      onCaptureEnd,
      getShouldRespectForcePress,
    }),
    [
      callbacks,
      getDraggableRef,
      getWindow,
      canStartCapturing,
      onCaptureStart,
      onCaptureEnd,
      getShouldRespectForcePress,
    ],
  );
  const onMouseDown = useMouseSensor(mouseArgs);

  const keyboardArgs: KeyboardSensorArgs = useMemo(
    () => ({
      callbacks,
      getDraggableRef,
      getWindow,
      canStartCapturing,
      onCaptureStart,
      onCaptureEnd,
    }),
    [
      callbacks,
      canStartCapturing,
      getDraggableRef,
      getWindow,
      onCaptureEnd,
      onCaptureStart,
    ],
  );
  const onKeyDown = useKeyboardSensor(keyboardArgs);

  const touchArgs: TouchSensorArgs = useMemo(
    () => ({
      callbacks,
      getDraggableRef,
      getWindow,
      canStartCapturing,
      getShouldRespectForcePress,
      onCaptureStart,
      onCaptureEnd,
    }),
    [
      callbacks,
      getDraggableRef,
      getWindow,
      canStartCapturing,
      getShouldRespectForcePress,
      onCaptureStart,
      onCaptureEnd,
    ],
  );
  const onTouchStart = useTouchSensor(touchArgs);

  // aborting on unmount

  useLayoutEffect(() => {
    // only when unmounting
    return () => {
      if (!capturingRef.current) {
        return;
      }
      abortCapture();

      if (lastArgsRef.current.isDragging) {
        // eslint-disable-next-line react-hooks/exhaustive-deps
        lastArgsRef.current.callbacks.onCancel();
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // No longer enabled but still capturing: need to abort and cancel if needed
  if (!isEnabled && capturingRef.current) {
    abortCapture();
    if (lastArgsRef.current.isDragging) {
      warning(
        'You have disabled dragging on a Draggable while it was dragging. The drag has been cancelled',
      );
      callbacks.onCancel();
    }
  }

  // Handle aborting
  // No longer dragging but still capturing: need to abort
  // Using a layout effect to ensure that there is a flip from isDragging => !isDragging
  // When there is a pending drag !isDragging will always be true
  useLayoutEffect(() => {
    if (!isDragging && capturingRef.current) {
      abortCapture();
    }
  }, [abortCapture, isDragging]);

  const props: ?DragHandleProps = useMemo(() => {
    if (!isEnabled) {
      return null;
    }
    return {
      onMouseDown,
      onKeyDown,
      onTouchStart,
      onFocus,
      onBlur,
      tabIndex: 0,
      'data-react-beautiful-dnd-drag-handle': styleContext,
      // English default. Consumers are welcome to add their own start instruction
      'aria-roledescription': 'Draggable item. Press space bar to lift',
      // Opting out of html5 drag and drops
      draggable: false,
      onDragStart: preventHtml5Dnd,
    };
  }, [
    isEnabled,
    onBlur,
    onFocus,
    onKeyDown,
    onMouseDown,
    onTouchStart,
    styleContext,
  ]);

  return props;
}
