import type { Position } from 'css-box-model';
import { invariant } from '../../invariant';
import { add, subtract } from '../position';
import {
  canScrollWindow,
  canScrollDroppable,
  getWindowOverlap,
  getDroppableOverlap,
} from './can-scroll';
import whatIsDraggedOver from '../droppable/what-is-dragged-over';
import type { MoveArgs } from '../action-creators';
import type {
  DroppableDimension,
  Viewport,
  DraggingState,
  DroppableId,
} from '../../types';

interface Args {
  scrollDroppable: (id: DroppableId, change: Position) => void;
  scrollWindow: (offset: Position) => void;
  move: (args: MoveArgs) => unknown;
}

export type JumpScroller = (state: DraggingState) => void;

type Remainder = Position;

export default ({
  move,
  scrollDroppable,
  scrollWindow,
}: Args): JumpScroller => {
  const moveByOffset = (state: DraggingState, offset: Position) => {
    const client: Position = add(state.current.client.selection, offset);
    move({ client });
  };

  const scrollDroppableAsMuchAsItCan = (
    droppable: DroppableDimension,
    change: Position,
  ): Remainder | null => {
    // Droppable cannot absorb any of the scroll
    if (!canScrollDroppable(droppable, change)) {
      return change;
    }

    const overlap: Position | null = getDroppableOverlap(droppable, change);

    // Droppable can absorb the entire change
    if (!overlap) {
      scrollDroppable(droppable.descriptor.id, change);
      return null;
    }

    // Droppable can only absorb a part of the change
    const whatTheDroppableCanScroll: Position = subtract(change, overlap);
    scrollDroppable(droppable.descriptor.id, whatTheDroppableCanScroll);

    const remainder: Position = subtract(change, whatTheDroppableCanScroll);
    return remainder;
  };

  const scrollWindowAsMuchAsItCan = (
    isWindowScrollAllowed: boolean,
    viewport: Viewport,
    change: Position,
  ): Position | null => {
    if (!isWindowScrollAllowed) {
      return change;
    }

    if (!canScrollWindow(viewport, change)) {
      // window cannot absorb any of the scroll
      return change;
    }

    const overlap: Position | null = getWindowOverlap(viewport, change);

    // window can absorb entire scroll
    if (!overlap) {
      scrollWindow(change);
      return null;
    }

    // window can only absorb a part of the scroll
    const whatTheWindowCanScroll: Position = subtract(change, overlap);
    scrollWindow(whatTheWindowCanScroll);

    const remainder: Position = subtract(change, whatTheWindowCanScroll);
    return remainder;
  };

  const jumpScroller: JumpScroller = (state: DraggingState) => {
    const request: Position | null = state.scrollJumpRequest;

    if (!request) {
      return;
    }

    const destination: DroppableId | null = whatIsDraggedOver(state.impact);
    invariant(
      destination,
      'Cannot perform a jump scroll when there is no destination',
    );

    // 1. We scroll the droppable first if we can to avoid the draggable
    // leaving the list

    const droppableRemainder: Position | null = scrollDroppableAsMuchAsItCan(
      state.dimensions.droppables[destination],
      request,
    );

    // droppable absorbed the entire scroll
    if (!droppableRemainder) {
      return;
    }

    const viewport: Viewport = state.viewport;
    const windowRemainder: Position | null = scrollWindowAsMuchAsItCan(
      state.isWindowScrollAllowed,
      viewport,
      droppableRemainder,
    );

    // window could absorb all the droppable remainder
    if (!windowRemainder) {
      return;
    }

    // The entire scroll could not be absorbed by the droppable and window
    // so we manually move whatever is left
    moveByOffset(state, windowRemainder);
  };

  return jumpScroller;
};
