import rafSchd from "raf-schd";
import { useCallback, useMemo, useState } from "react";
import { invariant } from "../../invariant";
import create from "./lock";
import canStartDrag from "../../state/can-start-drag";
import {
  drop as dropAction,
  flush,
  lift as liftAction,
  move as moveAction,
  moveDown as moveDownAction,
  moveLeft as moveLeftAction,
  moveRight as moveRightAction,
  moveUp as moveUpAction
} from "../../state/action-creators";
import useMouseSensor from "./sensors/use-mouse-sensor";
import useKeyboardSensor from "./sensors/use-keyboard-sensor";
import useTouchSensor from "./sensors/use-touch-sensor";
import useValidateSensorHooks from "./use-validate-sensor-hooks";
import isEventInInteractiveElement from "./is-event-in-interactive-element";
import getBorderBoxCenterPosition from "../get-border-box-center-position";
import { warning } from "../../dev-warning";
import useLayoutEffect from "../use-isomorphic-layout-effect";
import { noop } from "../../empty";
import findClosestDraggableIdFromEvent, {
  findClosestDragHandleFromEvent
} from "./find-closest-draggable-id-from-event";
import findDraggable from "../get-elements/find-draggable";
import bindEvents from "../event-bindings/bind-events";

function preventDefault(event) {
  event.preventDefault();
}

function isActive({ expected, phase, isLockActive, shouldWarn }) {
  // lock is no longer active
  if (!isLockActive()) {
    if (shouldWarn) {
      warning(`
        Cannot perform action.
        The sensor no longer has an action lock.

        Tips:

        - Throw away your action handlers when forceStop() is called
        - Check actions.isActive() if you really need to
      `);
    }
    return false;
  }
  // wrong phase
  if (expected !== phase) {
    if (shouldWarn) {
      warning(`
        Cannot perform action.
        The actions you used belong to an outdated phase

        Current phase: ${expected}
        You called an action from outdated phase: ${phase}

        Tips:

        - Do not use preDragActions actions after calling preDragActions.lift()
      `);
    }
    return false;
  }
  return true;
}

function canStart({ lockAPI, store, registry, draggableId }) {
  // lock is already claimed - cannot start
  if (lockAPI.isClaimed()) {
    return false;
  }
  const entry = registry.draggable.findById(draggableId);
  if (!entry) {
    warning(`Unable to find draggable with id: ${draggableId}`);
    return false;
  }

  // draggable is not enabled - cannot start
  if (!entry.options.isEnabled) {
    return false;
  }

  // Application might now allow dragging right now
  if (!canStartDrag(store.getState(), draggableId)) {
    return false;
  }
  return true;
}

function tryStart({ lockAPI, contextId, store, registry, draggableId, forceSensorStop, sourceEvent }) {
  const shouldStart = canStart({
    lockAPI,
    store,
    registry,
    draggableId
  });
  if (!shouldStart) {
    return null;
  }
  const entry = registry.draggable.getById(draggableId);
  const el = findDraggable(contextId, entry.descriptor.id);
  if (!el) {
    warning(`Unable to find draggable element with id: ${draggableId}`);
    return null;
  }

  // Do not allow dragging from interactive elements
  if (sourceEvent && !entry.options.canDragInteractiveElements && isEventInInteractiveElement(el, sourceEvent)) {
    return null;
  }

  // claiming lock
  const lock = lockAPI.claim(forceSensorStop || noop);
  let phase = "PRE_DRAG";

  function getShouldRespectForcePress() {
    // not looking up the entry as it might have been removed in a virtual list
    return entry.options.shouldRespectForcePress;
  }

  function isLockActive() {
    return lockAPI.isActive(lock);
  }

  function tryDispatch(expected, getAction) {
    if (
      isActive({
        expected,
        phase,
        isLockActive,
        shouldWarn: true
      })
    ) {
      store.dispatch(getAction());
    }
  }

  const tryDispatchWhenDragging = tryDispatch.bind(null, "DRAGGING");

  function lift(args) {
    function completed() {
      lockAPI.release();
      phase = "COMPLETED";
    }

    // Double lift = bad
    if (phase !== "PRE_DRAG") {
      completed();
      invariant(phase === "PRE_DRAG", `Cannot lift in phase ${phase}`);
    }
    store.dispatch(liftAction(args.liftActionArgs));

    // We are now in the DRAGGING phase
    phase = "DRAGGING";

    function finish(
      reason,
      options = {
        shouldBlockNextClick: false
      }
    ) {
      args.cleanup();

      // block next click if requested
      if (options.shouldBlockNextClick) {
        const unbind = bindEvents(window, [
          {
            eventName: "click",
            fn: preventDefault,
            options: {
              // only blocking a single click
              once: true,
              passive: false,
              capture: true
            }
          }
        ]);
        // Sometimes the click is swallowed, such as when there is reparenting
        // The click event (in the message queue) will occur before the next setTimeout expiry
        // https://codesandbox.io/s/click-behaviour-pkfk2
        setTimeout(unbind);
      }

      // releasing
      completed();
      store.dispatch(
        dropAction({
          reason
        })
      );
    }

    return {
      isActive: () =>
        isActive({
          expected: "DRAGGING",
          phase,
          isLockActive,
          // Do not want to want warnings for boolean checks
          shouldWarn: false
        }),
      shouldRespectForcePress: getShouldRespectForcePress,
      drop: (options) => finish("DROP", options),
      cancel: (options) => finish("CANCEL", options),
      ...args.actions
    };
  }

  function fluidLift(clientSelection) {
    const move = rafSchd((client) => {
      tryDispatchWhenDragging(() =>
        moveAction({
          client
        })
      );
    });
    const api = lift({
      liftActionArgs: {
        id: draggableId,
        clientSelection,
        movementMode: "FLUID"
      },
      cleanup: () => move.cancel(),
      actions: {
        move
      }
    });
    return {
      ...api,
      move
    };
  }

  function snapLift() {
    const actions = {
      moveUp: () => tryDispatchWhenDragging(moveUpAction),
      moveRight: () => tryDispatchWhenDragging(moveRightAction),
      moveDown: () => tryDispatchWhenDragging(moveDownAction),
      moveLeft: () => tryDispatchWhenDragging(moveLeftAction)
    };
    return lift({
      liftActionArgs: {
        id: draggableId,
        clientSelection: getBorderBoxCenterPosition(el),
        movementMode: "SNAP"
      },
      cleanup: noop,
      actions
    });
  }

  function abortPreDrag() {
    const shouldRelease = isActive({
      expected: "PRE_DRAG",
      phase,
      isLockActive,
      shouldWarn: true
    });
    if (shouldRelease) {
      lockAPI.release();
    }
  }

  const preDrag = {
    isActive: () =>
      isActive({
        expected: "PRE_DRAG",
        phase,
        isLockActive,
        // Do not want to want warnings for boolean checks
        shouldWarn: false
      }),
    shouldRespectForcePress: getShouldRespectForcePress,
    fluidLift,
    snapLift,
    abort: abortPreDrag
  };
  return preDrag;
}

// default sensors are now exported to library consumers
const defaultSensors = [useMouseSensor, useKeyboardSensor, useTouchSensor];
export default function useSensorMarshal({ contextId, store, registry, customSensors, enableDefaultSensors }) {
  const useSensors = [...(enableDefaultSensors ? defaultSensors : []), ...(customSensors || [])];
  const lockAPI = useState(() => create())[0];
  const tryAbandonLock = useCallback(
    function tryAbandonLock(previous, current) {
      if (previous.isDragging && !current.isDragging) {
        lockAPI.tryAbandon();
      }
    },
    [lockAPI]
  );

  // We need to abort any capturing if there is no longer a drag
  useLayoutEffect(
    function listenToStore() {
      let previous = store.getState();
      const unsubscribe = store.subscribe(() => {
        const current = store.getState();
        tryAbandonLock(previous, current);
        previous = current;
      });

      // unsubscribe from store when unmounting
      return unsubscribe;
    },
    [lockAPI, store, tryAbandonLock]
  );

  // abort any lock on unmount
  useLayoutEffect(() => {
    return lockAPI.tryAbandon;
  }, [lockAPI.tryAbandon]);
  const canGetLock = useCallback(
    (draggableId) => {
      return canStart({
        lockAPI,
        registry,
        store,
        draggableId
      });
    },
    [lockAPI, registry, store]
  );
  const tryGetLock = useCallback(
    (draggableId, forceStop, options) =>
      tryStart({
        lockAPI,
        registry,
        contextId,
        store,
        draggableId,
        forceSensorStop: forceStop,
        sourceEvent: options && options.sourceEvent ? options.sourceEvent : null
      }),
    [contextId, lockAPI, registry, store]
  );

  const findClosestDragHandle = useCallback((event) => findClosestDragHandleFromEvent(contextId, event), [contextId]);

  const findClosestDraggableId = useCallback((event) => findClosestDraggableIdFromEvent(contextId, event), [contextId]);
  const findOptionsForDraggable = useCallback(
    (id) => {
      const entry = registry.draggable.findById(id);
      return entry ? entry.options : null;
    },
    [registry.draggable]
  );
  const tryReleaseLock = useCallback(
    function tryReleaseLock() {
      if (!lockAPI.isClaimed()) {
        return;
      }
      lockAPI.tryAbandon();
      if (store.getState().phase !== "IDLE") {
        store.dispatch(flush());
      }
    },
    [lockAPI, store]
  );

  // TODO: Changed to accommodate ESLINT
  const isLockClaimed = useCallback(() => lockAPI.isClaimed(), [lockAPI]);

  const api = useMemo(
    () => ({
      canGetLock,
      tryGetLock,
      findClosestDraggableId,
      findOptionsForDraggable,
      tryReleaseLock,
      isLockClaimed,
      findClosestDragHandle
    }),
    [
      canGetLock,
      tryGetLock,
      findClosestDraggableId,
      findOptionsForDraggable,
      tryReleaseLock,
      isLockClaimed,
      findClosestDragHandle
    ]
  );

  // Bad ass
  useValidateSensorHooks(useSensors);
  for (let i = 0; i < useSensors.length; i++) {
    useSensors[i](api);
  }
}
