import { invariant } from "../../../invariant";
import { animateDrop, completeDrop, dropPending } from "../../action-creators";
import { isEqual } from "../../position";
import getDropDuration from "./get-drop-duration";
import getNewHomeClientOffset from "./get-new-home-client-offset";
import getDropImpact from "./get-drop-impact";
import { tryGetCombine, tryGetDestination } from "../../get-impact-location";

const dropMiddleware =
  ({ getState, dispatch }) =>
  (next) =>
  (action) => {
    if (action.type !== "DROP") {
      next(action);
      return;
    }
    const state = getState();
    const reason = action.payload.reason;

    // Still waiting for a bulk collection to publish
    // We are now shifting the application into the 'DROP_PENDING' phase
    if (state.phase === "COLLECTING") {
      dispatch(
        dropPending({
          reason
        })
      );
      return;
    }

    // Could have occurred in response to an error
    if (state.phase === "IDLE") {
      return;
    }

    // Still waiting for our drop pending to end
    // TODO: should this throw?
    const isWaitingForDrop = state.phase === "DROP_PENDING" && state.isWaiting;
    invariant(!isWaitingForDrop, "A DROP action occurred while DROP_PENDING and still waiting");
    invariant(state.phase === "DRAGGING" || state.phase === "DROP_PENDING", `Cannot drop in phase: ${state.phase}`);
    // We are now in the DRAGGING or DROP_PENDING phase

    const critical = state.critical;
    const dimensions = state.dimensions;
    const draggable = dimensions.draggables[state.critical.draggable.id];
    // Only keeping impact when doing a user drop - otherwise we are cancelling

    const { impact, didDropInsideDroppable } = getDropImpact({
      reason,
      lastImpact: state.impact,
      afterCritical: state.afterCritical,
      onLiftImpact: state.onLiftImpact,
      home: state.dimensions.droppables[state.critical.droppable.id],
      viewport: state.viewport,
      draggables: state.dimensions.draggables
    });

    // only populating destination / combine if 'didDropInsideDroppable' is true
    const destination = didDropInsideDroppable ? tryGetDestination(impact) : null;
    const combine = didDropInsideDroppable ? tryGetCombine(impact) : null;
    const source = {
      index: critical.draggable.index,
      droppableId: critical.droppable.id
    };
    const result = {
      draggableId: draggable.descriptor.id,
      type: draggable.descriptor.type,
      source,
      reason,
      mode: state.movementMode,
      // destination / combine will be null if didDropInsideDroppable is true
      destination,
      combine
    };
    const newHomeClientOffset = getNewHomeClientOffset({
      impact,
      draggable,
      dimensions,
      viewport: state.viewport,
      afterCritical: state.afterCritical
    });
    const completed = {
      critical: state.critical,
      afterCritical: state.afterCritical,
      result,
      impact
    };
    const isAnimationRequired =
      // 1. not already in the right spot
      !isEqual(state.current.client.offset, newHomeClientOffset) ||
      // 2. doing a combine (we still want to animate the scale and opacity fade)
      // looking at the result and not the impact as the combine impact is cleared
      Boolean(result.combine);
    if (!isAnimationRequired) {
      dispatch(
        completeDrop({
          completed
        })
      );
      return;
    }
    const dropDuration = getDropDuration({
      current: state.current.client.offset,
      destination: newHomeClientOffset,
      reason
    });
    const args = {
      newHomeClientOffset,
      dropDuration,
      completed
    };
    dispatch(animateDrop(args));
  };

export default dropMiddleware;
