import type { Position } from 'css-box-model';
import { invariant } from '../../../invariant';
import type {
  State,
  DropReason,
  Critical,
  DraggableLocation,
  DropResult,
  CompletedDrag,
  Combine,
  DimensionMap,
  DraggableDimension,
} from '../../../types';
import type { Middleware } from '../../store-types';
import {
  animateDrop,
  completeDrop,
  dropPending,
  guard,
} from '../../action-creators';
import type { AnimateDropArgs } 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 type { Result } from './get-drop-impact';
import { tryGetCombine, tryGetDestination } from '../../get-impact-location';

const dropMiddleware: Middleware =
  ({ getState, dispatch }) =>
  (next) =>
  (action) => {
    if (!guard(action, 'DROP')) {
      next(action);
      return;
    }

    const state: State = getState();
    const reason: DropReason = 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: boolean =
      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: Critical = state.critical;
    const dimensions: DimensionMap = state.dimensions;
    const draggable: DraggableDimension =
      dimensions.draggables[state.critical.draggable.id];
    // Only keeping impact when doing a user drop - otherwise we are cancelling

    const { impact, didDropInsideDroppable }: Result = 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: DraggableLocation | null = didDropInsideDroppable
      ? tryGetDestination(impact)
      : null;
    const combine: Combine | null = didDropInsideDroppable
      ? tryGetCombine(impact)
      : null;

    const source: DraggableLocation = {
      index: critical.draggable.index,
      droppableId: critical.droppable.id,
    };

    const result: DropResult = {
      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: Position = getNewHomeClientOffset({
      impact,
      draggable,
      dimensions,
      viewport: state.viewport,
      afterCritical: state.afterCritical,
    });

    const completed: CompletedDrag = {
      critical: state.critical,
      afterCritical: state.afterCritical,
      result,
      impact,
    };

    const isAnimationRequired: boolean =
      // 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: number = getDropDuration({
      current: state.current.client.offset,
      destination: newHomeClientOffset,
      reason,
    });

    const args: AnimateDropArgs = {
      newHomeClientOffset,
      dropDuration,
      completed,
    };

    dispatch(animateDrop(args));
  };

export default dropMiddleware;
